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 composants d'un processeur 0 65784 763497 742749 2026-04-11T19:17:02Z Mewtow 31375 /* La micro-architecture d'un processeur */ 763497 wikitext text/x-wiki Dans le chapitre sur le langage machine, on a vu le processeur comme une espèce de boite noire contenant des registres qui exécutait des instructions les unes après les autres. Mais on n'a pas encore vu ce qu'il y a dans la boite noire. Pour cela, nous allons attaquer la '''micro-architecture''' du processeur, ce qu'il y a à l'intérieur et comment il fait pour exécuter une instruction. [[File:Von Neumann Cyclus.png|vignette|Les trois cycles d'une instruction.]] Le but d'un processeur, c'est d’exécuter des instructions. Cela nécessite de faire quelques manipulations assez spécifiques et qui sont toutes les mêmes quel que soit l'ordinateur. Exécuter une instruction est un processeur en trois étapes : * Le processeur charge l'instruction depuis la mémoire : c'est l'étape de '''chargement''' (''Fetch'') ; * Ensuite, le processeur « étudie » la suite de bits de l'instruction et en déduit quelle est l'instruction à exécuter : c'est l'étape de '''décodage''' (''Decode'') ; * Enfin, le processeur exécute l'instruction : c'est l'étape d’'''exécution''' (''Execute'').* Une quatrième '''étape d'interruption''' s'occupe des interruptions, précisément des interruptions matérielles et des exceptions. Elle est optionnelle, car de rares processeurs ne supportent pas les interruptions. Nous ne parlerons pas de l'étape d'interruption ici et allons nous concentrer sur les trois premières étapes. Il se trouve qu'elles sont réalisées chacune par un circuit séparé des autres. ==La micro-architecture d'un processeur== A l'intérieur du processeur, il y a un circuit dédié pour le chargement de l'instruction, un circuit dédié au décodage des instruction, et un autre pour leur exécution. Ils portent respectivement les noms d'unité de chargement, unité de décodage d'instruction, et de chemin de données. * Le '''chemin de données''' contient de quoi exécuter les instructions. Il regroupe des circuits de calcul, les registres, un circuit de communication avec la mémoire, et des interconnexions entre les circuits précédents. * L’'''unité de contrôle''' regroupe l'unité de chargement et le décodeur d'instruction. L'unité de chargement charge l'instruction depuis la mémoire, le décodeur/séquenceur commande le chemin de données. Les deux circuits sont séparés, mais ils communiquent entre eux pour gérer les branchements, pour charger les instructions au bon moment, etc. [[File:Microarchitecture d'un processeur.png|centre|vignette|upright=2|Microarchitecture d'un processeur]] ===Le chemin de données=== Pour effectuer des calculs, le processeur contient un circuit spécialisé : l''''unité de calcul'''. De plus, le processeur contient des '''registres''', ainsi qu'un circuit d''''interface mémoire'''. Les registres, l'unité de calcul, et l'interface mémoire sont reliés entre eux par un ensemble d'interconnexions, afin de pouvoir échanger des informations. Ces interconnexions forment ce qu'on appelle le '''bus interne du processeur'''. L'ensemble formé par ces composants s’appelle le '''chemin de données'''. Son nom vient du fait que les données circulent dans le chemin de données, dans le bus interne, l'unité de calcul, les registres et l'unité mémoire. Pour le dire autrement, il regroupe tous les circuits dans lesquels sont traités des données. [[File:Chemin de données.png|centre|vignette|upright=1.5|Chemin de données]] L'unité de calcul, les registres et l'unité mémoire sont configurables. Par exemple, l'unité de calcul peut faire plusieurs opérations : addition, soustraction, opérations logiques, et bien d'autres. Mais il faut préciser quelle opération effectuer. Pour cela, l'ALU a une entrée de commande sur laquelle on précise l'opération à effectuer. L'opération est encodée en binaire, chaque opération a son propre numéro. Il s'agit d'un '''signal de commande''', qui indique comment configurer l'unité de calcul pour faire ce qui est demandé. Les registres et l'unité mémoire ont aussi leurs propres signaux de commande dédiés. Par exemple, les noms/numéros de registres sont des signaux de commande qui permettent de sélectionner les registres utilisés comme opérande ou pour enregistrer le résultat. De même, l'unité mémoire ne fonctionne que si on lui précise quelle est l'adresse à lire/écrire, et s'il faut faire une lecture ou une écriture : les deux sont des signaux de commande. [[File:Signaux de commandes CPU.png|centre|vignette|upright=2|Signaux de commandes CPU]] ===Le séquenceur : signaux de commande et micro-opérations=== Le chemin de données est en charge de l'exécution des instructions, au minimum (on verra qu'il peut être impliqué dans l'étape de chargement, dans quelques paragraphes). Intuitivement, exécuter une instruction revient à configurer le chemin de données de manière adéquate. Si on veut faire une addition entre un registre et une adresse mémoire, on configure l'unité de calcul pour faire une addition, on configure les registres de manière à sélectionner les registres opérande/destination, et on envoie l'adresse à l'unité mémoire. Le '''décodeur d'instruction''' détermine quels sont les signaux de commande adéquats, à partir de l'instruction machine. Les signaux de commande des registres sont déterminés à partir des noms/numéros de registre encodés dans l'instruction, les signaux pour l'ALU sont déterminés à partir de l'opcode, etc. Le terme décodeur trahit le fait qu'il traduit une instruction machine en signaux de commandes séparés, qui se déduisent de l'instruction elle-même, de son encodage. [[File:Intérieur d'un processeur.png|centre|vignette|upright=2|Intérieur d'un processeur]] Mais cette explication ne marche que pour des instructions simples, qui, s'effectuent en une seule étape. Or, l'exécution d'une instruction complexe se fait en plusieurs étapes distinctes. Suivant l'instruction machine, le nombre d'étapes n'est pas le même. Voyons quelles instructions tendent à se faire une une ou plusieurs étapes, avec quelques exemples. Commençons par l'exemple d'une instruction de lecture en mode d'adressage absolu. L'adresse à lire est envoyée à l'unité mémoire directement, le nom de registre est envoyé aux registres et basta. L’exécution de l'instruction se fait donc en une seule étape : la lecture proprement dite. Maintenant, voyons la même instruction, mais avec un mode d'adressage base + indice. Avec ce mode d'adressage, l'adresse doit être calculée en additionnant une adresse de base et d'un indice, les deux étant stockés dans des registres. En plus de devoir lire la donnée, l'instruction va devoir calculer l'adresse, dans l'unité de calcul. L'étape d’exécution s'effectue dorénavant en deux étapes : une étape de calcul d'adresse, suivie de la lecture en mémoire. Bref, on voit bien que l’exécution d'une instruction s'effectue en plusieurs étapes distinctes, qui vont soit faire un calcul, soit échanger des données entre registres, soit communiquer avec la RAM. Chaque étape s'appelle une '''micro-opération''', ou encore une micro-instruction. Les micro-opérations se résument à des opérations basiques : échange de données entre registres, calcul sur l'ALU, accès mémoire. Chaque micro-opération encode les signaux de commande à destination du chemin de données, pour le configurer et le commander. Pour simplifier, une micro-opération est encodée en concaténant les signaux de commande pour l'ALU, ceux pour les registres, pour l'unité mémoire, etc. {|class="wikitable" |- ! colspan="4" | Micro-opération, encodage en binaire |- | Signaux de commande pour l'ALU | Signaux de commande pour les registres | Signaux de commande pour l'unité d'accès mémoire | Signaux de commande autres |} Toute instruction machine est équivalente à une suite de micro-opérations exécutée dans un ordre précis. Dit autrement, chaque instruction machine est traduite en suite de micro-opérations à chaque fois qu'on l’exécute. C'est le décodeur d'instruction qui transforme une instruction machine en une série de micro-opérations. Pour cela, le décodeur devient un circuit séquentiel. Il est parfois appelé le '''séquenceur''', terme qui trahit bien le fait qu'il traduit une instruction machine en une séquence de micro-opérations. [[File:Micro-operations.svg|centre|vignette|upright=2|Micro-operations]] Certaines µinstructions font un cycle d'horloge, alors que d'autres peuvent prendre plusieurs cycles. Un accès mémoire en RAM peut prendre 200 cycles d'horloge et ne représenter qu'une seule µinstruction, par exemple. Même chose pour certaines opérations de calcul, comme des divisions ou multiplication, qui correspondent à une seule µinstruction mais prennent plusieurs cycles. Typiquement, les instructions des processeurs RISC se font en une seule micro-opération, alors que les instructions complexes des processeurs CISC demandent plusieurs micro-opérations, particulièrement celles qui demandent de faire des accès mémoire ou qui utilisent des modes d'adressage complexes. Ce n'est pas une règle absolue, quelques instructions RISC se font en plusieurs micro-opérations, de même que les processeurs CISC ont des instructions simples qui se font en une seule micro-opération. Mais la corrélation est assez bonne. En conséquence, les processeurs RISC ont des décodeurs d'instruction très simples. ===L'unité de chargement=== L''''unité de chargement''' est placée avant le décodeur/séquenceur. Son rôle est de charger les instructions les unes après les autres, dans le bon ordre. Pour exécuter une suite d'instructions dans le bon ordre, le processeur doit savoir quelle est la prochaine instruction à exécuter : il doit donc contenir une mémoire qui stocke cette information. C'est le rôle du registre d'adresse d'instruction, aussi appelé '''''program counter'''''. L'adresse de la prochaine instruction ne sort pas de nulle part : on peut la déduire de l'adresse de l'instruction en cours d’exécution par divers moyens plus ou moins simples. Généralement, on profite du fait que le programmeur/compilateur place les instructions les unes à la suite des autres en mémoire, dans l'ordre où elles doivent être exécutées. Ainsi, on peut calculer l'adresse de la prochaine instruction en ajoutant la longueur de l'instruction chargée au ''program counter''. Mais sur d'autres processeurs, chaque instruction précise l'adresse de la suivante. Ces processeurs n'ont pas besoin de calculer une adresse qui leur est fournie sur un plateau d'argent. Sur de tels processeurs, chaque instruction précise quelle est la prochaine instruction, directement dans la suite de bit représentant l'instruction en mémoire. Les processeurs de ce type contiennent toujours un registre d'adresse d'instruction, pour faciliter l’interfaçage avec le bus d'adresse. La partie de l'instruction stockant l'adresse de la prochaine instruction est alors recopiée dans ce registre, pour faciliter sa copie sur le bus d'adresse. Mais le compteur ordinal n'existe pas. Sur des processeurs aussi bizarres, pas besoin de stocker les instructions en mémoire dans l'ordre dans lesquelles elles sont censées être exécutées. Mais ces processeurs sont très très rares et peuvent être considérés comme des exceptions à la règle. [[File:Encodage d'une instruction sur un processeur sans Program Counter.png|centre|vignette|upright=2|Encodage d'une instruction sur un processeur sans Program Counter.]] L'unité de chargement s'occupe de tout ce qui a trait au ''program counter'', mais aussi de l'accès mémoire pour charger l'instruction. Il faut noter que sur certains processeurs, le chargement d'une instruction machine est une opération réalisée par le chemin de données. C'est le cas sur les architectures Von Neumann, où il n'y a qu'un seul bus mémoire, qui sert à la fois pour lire/écrire des données et pour charger des instructions. Dans ce cas, l'unité de communication avec la mémoire s'occupe aussi de charger les instructions. Le chargement d'une instruction est alors réalisée par une micro-opération d'accès mémoire, qui copie l'instruction chargée dans un registre dédié, appelé le registre d'instruction, lui-même relié au séquenceur. ===Les autres circuits=== Un processeur contient au minimum une unité de chargement, le chemin de données et le décodeur d'instruction. Mis c'est là le minimum, un processeur peut parfaitement contenir d'autres circuits en plus. Par exemple, il peut intégrer des circuits pour gérer les interruptions matérielles, des circuits ''timer'' pour compter des durées, des circuits dit DMA pour communiquer avec les périphériques, etc. L'exemple du 80186 d'Intel est illustré ci-dessous. Il est composé de deux circuits principaux : une ''Execution Unit'' qui regroupe l'unité de calcul et les registres, une ''Bus Interface Unit'' qui regroupe unité mémoire, unité de chargement et séquenceur. Mais en plus de ces deux circuits principaux, le processeur incorpore des circuits ''timers'', de quoi gérer les interruption, un circuit DMA, et bien d'autres encore. [[File:Intel 80186 80188 arch.svg|centre|vignette|upright=2.5|Intel 80186 80188 arch]] ==Des processeurs vendus en kit aux premiers microprocesseurs== Un processeur est un circuit assez complexe et qui utilise beaucoup de transistors. Avant les années 1970, il n'était pas possible de produire un processeur en un seul morceau. Impossible de mettre un processeur dans un seul boitier. Les tout premiers processeurs étaient fabriqués porte logique par porte logique et comprenaient plusieurs milliers de boitiers reliés entre eux. Par la suite, les progrès de la miniaturisation permirent de faire des pièces plus grandes. L'invention du microprocesseur permis de placer tout le processeur dans un seul boitier, une seule puce électronique. ===Avant l'invention du microprocesseur=== Avant l'invention du microprocesseur, les processeurs étaient fournis en pièces détachées qu'il fallait relier entre elles. Le processeur était composé de plusieurs circuits intégrés, placés sur la même carte mère et connectés ensemble par des fils métalliques. Un exemple de processeur conçu en kit est la série des Intel 3000. Elle regroupe plusieurs circuits séparés : l'Intel 3001 est le séquenceur, l'Intel 3002 est le chemin de données (ALU et registres), le 3003 est un circuit d'anticipation de retenue censé être combiné avec l'ALU, le 3212 est une mémoire tampon, le 3214 est une unité de gestion des interruptions, les 3216/3226 sont des interfaces de bus mémoire. On pourrait aussi citer la famille de circuits intégrés AMD Am2900. Les ALUs en pièces détachées de l'époque étaient assez simples et géraient 2, 4, 8 bits, rarement 16 bits. Et il était possible d'assembler plusieurs ALU pour créer des ALU plus grandes, par exemple combiner plusieurs ALU 4 bits afin de créer une unité de calcul 8 bits, 12 bits, 16 bits, etc. Il s'agit de la méthode du '''''bit slicing''''' que nous avions abordée dans le chapitre sur les unités de calcul. ===L'intel 4004 : le premier microprocesseur=== Par la suite, les progrès de la miniaturisation ont permis de mettre un processeur entier dans un seul circuit intégré. C'est ainsi que sont nés les '''microprocesseurs''', à savoir des processeurs qui tiennent tout entier sur une seule puce de silicium. Les tout premiers microprocesseurs étaient des processeurs à application militaire, comme le processeur du F-14 CADC ou celui de l'''Air data computer''. Le tout premier microprocesseur commercialisé au grand public est le 4004 d'Intel, sorti en 1971. Il comprenait environ 2300 transistors, avait une fréquence de 740 MHz, et manipulait des entiers de 4 bits. De plus, le processeur manipulait des entiers en BCD, ce qui fait qu'il pouvait manipuler un chiffre BCD à la fois (un chiffre BCD est codé sur 4 bits). Il pouvait faire 46 opérations différentes. C'était au départ un processeur de commande, prévu pour être intégré dans la calculatrice Busicom calculator 141-P, mais il fut utilisé pour d'autres applications quelque temps plus tard. Son successeur, l'Intel 4040, garda ces caractéristiques et n'apportait que quelques améliorations mineures : plus de registres, plus d'opérations, etc. Immédiatement après le 4004, les premiers microprocesseurs 8 bits furent commercialisés. Le 4004 fut suivi par le 8008 et quelques autres processeurs 8 bits extrêmement connus, comme le 8080 d'Intel, le 68000 de Motorola, le 6502 ou le Z80. Ces processeurs utilisaient là encore des boitiers similaires au 4004, mais avec plus de broches, vu qu'ils étaient passés de 4 à 8 bits. Par exemple, le 8008 utilisait 18 broches, le 8080 était une version améliorée du 8008 avec 40 broches. Le 8086 fut le premier processeur 16 bits. ===L'évolution des processeurs dans le temps=== La miniaturisation a eu des conséquences notables sur la manière dont sont conçus les processeurs, les mémoires et tous les circuits électroniques en général. On pourrait croire que la miniaturisation a entrainé une augmentation de la complexité des processeurs avec le temps, mais les choses sont à nuancer. Certes, on peut faire beaucoup plus de choses avec un milliard de transistors qu'avec seulement 10000 transistors, ce qui fait que les puces modernes sont d'une certaine manière plus complexes. Mais les anciens processeurs avaient une complexité cachée liée justement au faible nombre de transistors. Il est difficile de concevoir des circuits avec un faible nombre de transistors, ce qui fait que les fabricants de processeurs devaient utiliser des ruses de sioux pour économiser des transistors. Les circuits des processeurs étaient ainsi fortement optimisés pour économiser des portes logiques, à tous les niveaux. Les circuits les plus simples étaient optimisés à mort, on évitait de dupliquer des circuits, on partageait les circuits au maximum, etc. La conception interne de ces processeurs était simple au premier abord, mais avec quelques pointes de complexité dispersées dans toute la puce. De nos jours, les processeurs n'ont plus à économiser du transistor et le résultat est à double tranchant. Certes, ils n'ont plus à utiliser des optimisations pour économiser du circuit, mais ils vont au contraire utiliser leurs transistors pour rendre le processeur plus rapide. Beaucoup des techniques que nous verrons dans ce cours, comme l’exécution dans le désordre, le renommage de registres, les mémoires caches, la présence de plusieurs circuits de calcul, et bien d'autres ; améliorent les performances du processeur en ajoutant des circuits en plus. De plus, on n'hésite plus à dupliquer des circuits qu'on aurait autrefois mis en un seul exemplaire partagé. Tout cela rend le processeur plus complexe à l'intérieur. Une autre contrainte est la facilité de programmation. Les premiers processeurs devaient faciliter au plus la vie du programmeur. Il s'agissait d'une époque où on programmait en assembleur, c'est à dire en utilisant directement les instructions du processeur ! Les processeurs de l'époque utilisaient des jeu d'instruction CISC pour faciliter la vie du programmeur. Pourtant, ils avaient aussi des caractéristiques gênantes pour les programmeurs qui s'expliquent surtout par le faible nombre de transistors de l'époque : peu de registres, registres spécialisés, architectures à pile ou à accumulateur, etc. Ces processeurs étaient assez étranges pour les programmeurs : très simples sur certains points, difficiles pour d'autres. Les processeurs modernes ont d'autres contraintes. Grâce à la grande quantité de transistors dont ils disposent, ils incorporent des caractéristiques qui les rendent plus simples à programmer et à comprendre (registres banalisés, architectures LOAD-STORE, beaucoup de registres, moins d'instructions complexes, autres). De plus, si on ne programme plus les processeurs à la main, les langages de haut niveau passe par des compilateurs qui eux, programment le processeur. Leur interface avec le logiciel a été simplifiée pour coller au mieux avec ce que savent faire les compilateurs. En conséquence, l’interface logicielle des processeurs modernes est paradoxalement plus minimaliste que pour les vieux processeurs. Tout cela pour dire que la conception d'un processeur est une affaire de compromis, comme n'importe quelle tâche d'ingénierie. Il n'y a pas de solution parfaite, pas de solution miracle, juste différentes manières de faire qui collent plus ou moins avec la situation. Et les compromis changent avec l'époque et l'évolution de la technologie. Les technologies sont toutes interdépendantes, chaque évolution concernant les transistors influence la conception des puces électroniques, les technologies architecturales utilisées, ce qui influence l'interface avec le logiciel, ce qui influence ce qu'il est possible de faire en logiciel. Et inversement, les contraintes du logiciel influencent les niveaux les plus bas, et ainsi de suite. Cette morale nous suivra dans le reste du cours, où nous verrons qu'il est souvent possible de résoudre un problème de plusieurs manières différentes, toutes utiles, mais avec des avantages et inconvénients différents. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les interruptions et exceptions | prevText=Les interruptions et exceptions | next=Le chemin de données | nextText=Le chemin de données }} </noinclude> 99a9mnjew43cmmccuekcr454xsbpl7q 763498 763497 2026-04-11T19:18:09Z Mewtow 31375 /* L'unité de chargement */ 763498 wikitext text/x-wiki Dans le chapitre sur le langage machine, on a vu le processeur comme une espèce de boite noire contenant des registres qui exécutait des instructions les unes après les autres. Mais on n'a pas encore vu ce qu'il y a dans la boite noire. Pour cela, nous allons attaquer la '''micro-architecture''' du processeur, ce qu'il y a à l'intérieur et comment il fait pour exécuter une instruction. [[File:Von Neumann Cyclus.png|vignette|Les trois cycles d'une instruction.]] Le but d'un processeur, c'est d’exécuter des instructions. Cela nécessite de faire quelques manipulations assez spécifiques et qui sont toutes les mêmes quel que soit l'ordinateur. Exécuter une instruction est un processeur en trois étapes : * Le processeur charge l'instruction depuis la mémoire : c'est l'étape de '''chargement''' (''Fetch'') ; * Ensuite, le processeur « étudie » la suite de bits de l'instruction et en déduit quelle est l'instruction à exécuter : c'est l'étape de '''décodage''' (''Decode'') ; * Enfin, le processeur exécute l'instruction : c'est l'étape d’'''exécution''' (''Execute'').* Une quatrième '''étape d'interruption''' s'occupe des interruptions, précisément des interruptions matérielles et des exceptions. Elle est optionnelle, car de rares processeurs ne supportent pas les interruptions. Nous ne parlerons pas de l'étape d'interruption ici et allons nous concentrer sur les trois premières étapes. Il se trouve qu'elles sont réalisées chacune par un circuit séparé des autres. ==La micro-architecture d'un processeur== A l'intérieur du processeur, il y a un circuit dédié pour le chargement de l'instruction, un circuit dédié au décodage des instruction, et un autre pour leur exécution. Ils portent respectivement les noms d'unité de chargement, unité de décodage d'instruction, et de chemin de données. * Le '''chemin de données''' contient de quoi exécuter les instructions. Il regroupe des circuits de calcul, les registres, un circuit de communication avec la mémoire, et des interconnexions entre les circuits précédents. * L’'''unité de contrôle''' regroupe l'unité de chargement et le décodeur d'instruction. L'unité de chargement charge l'instruction depuis la mémoire, le décodeur/séquenceur commande le chemin de données. Les deux circuits sont séparés, mais ils communiquent entre eux pour gérer les branchements, pour charger les instructions au bon moment, etc. [[File:Microarchitecture d'un processeur.png|centre|vignette|upright=2|Microarchitecture d'un processeur]] ===Le chemin de données=== Pour effectuer des calculs, le processeur contient un circuit spécialisé : l''''unité de calcul'''. De plus, le processeur contient des '''registres''', ainsi qu'un circuit d''''interface mémoire'''. Les registres, l'unité de calcul, et l'interface mémoire sont reliés entre eux par un ensemble d'interconnexions, afin de pouvoir échanger des informations. Ces interconnexions forment ce qu'on appelle le '''bus interne du processeur'''. L'ensemble formé par ces composants s’appelle le '''chemin de données'''. Son nom vient du fait que les données circulent dans le chemin de données, dans le bus interne, l'unité de calcul, les registres et l'unité mémoire. Pour le dire autrement, il regroupe tous les circuits dans lesquels sont traités des données. [[File:Chemin de données.png|centre|vignette|upright=1.5|Chemin de données]] L'unité de calcul, les registres et l'unité mémoire sont configurables. Par exemple, l'unité de calcul peut faire plusieurs opérations : addition, soustraction, opérations logiques, et bien d'autres. Mais il faut préciser quelle opération effectuer. Pour cela, l'ALU a une entrée de commande sur laquelle on précise l'opération à effectuer. L'opération est encodée en binaire, chaque opération a son propre numéro. Il s'agit d'un '''signal de commande''', qui indique comment configurer l'unité de calcul pour faire ce qui est demandé. Les registres et l'unité mémoire ont aussi leurs propres signaux de commande dédiés. Par exemple, les noms/numéros de registres sont des signaux de commande qui permettent de sélectionner les registres utilisés comme opérande ou pour enregistrer le résultat. De même, l'unité mémoire ne fonctionne que si on lui précise quelle est l'adresse à lire/écrire, et s'il faut faire une lecture ou une écriture : les deux sont des signaux de commande. [[File:Signaux de commandes CPU.png|centre|vignette|upright=2|Signaux de commandes CPU]] ===Le séquenceur : signaux de commande et micro-opérations=== Le chemin de données est en charge de l'exécution des instructions, au minimum (on verra qu'il peut être impliqué dans l'étape de chargement, dans quelques paragraphes). Intuitivement, exécuter une instruction revient à configurer le chemin de données de manière adéquate. Si on veut faire une addition entre un registre et une adresse mémoire, on configure l'unité de calcul pour faire une addition, on configure les registres de manière à sélectionner les registres opérande/destination, et on envoie l'adresse à l'unité mémoire. Le '''décodeur d'instruction''' détermine quels sont les signaux de commande adéquats, à partir de l'instruction machine. Les signaux de commande des registres sont déterminés à partir des noms/numéros de registre encodés dans l'instruction, les signaux pour l'ALU sont déterminés à partir de l'opcode, etc. Le terme décodeur trahit le fait qu'il traduit une instruction machine en signaux de commandes séparés, qui se déduisent de l'instruction elle-même, de son encodage. [[File:Intérieur d'un processeur.png|centre|vignette|upright=2|Intérieur d'un processeur]] Mais cette explication ne marche que pour des instructions simples, qui, s'effectuent en une seule étape. Or, l'exécution d'une instruction complexe se fait en plusieurs étapes distinctes. Suivant l'instruction machine, le nombre d'étapes n'est pas le même. Voyons quelles instructions tendent à se faire une une ou plusieurs étapes, avec quelques exemples. Commençons par l'exemple d'une instruction de lecture en mode d'adressage absolu. L'adresse à lire est envoyée à l'unité mémoire directement, le nom de registre est envoyé aux registres et basta. L’exécution de l'instruction se fait donc en une seule étape : la lecture proprement dite. Maintenant, voyons la même instruction, mais avec un mode d'adressage base + indice. Avec ce mode d'adressage, l'adresse doit être calculée en additionnant une adresse de base et d'un indice, les deux étant stockés dans des registres. En plus de devoir lire la donnée, l'instruction va devoir calculer l'adresse, dans l'unité de calcul. L'étape d’exécution s'effectue dorénavant en deux étapes : une étape de calcul d'adresse, suivie de la lecture en mémoire. Bref, on voit bien que l’exécution d'une instruction s'effectue en plusieurs étapes distinctes, qui vont soit faire un calcul, soit échanger des données entre registres, soit communiquer avec la RAM. Chaque étape s'appelle une '''micro-opération''', ou encore une micro-instruction. Les micro-opérations se résument à des opérations basiques : échange de données entre registres, calcul sur l'ALU, accès mémoire. Chaque micro-opération encode les signaux de commande à destination du chemin de données, pour le configurer et le commander. Pour simplifier, une micro-opération est encodée en concaténant les signaux de commande pour l'ALU, ceux pour les registres, pour l'unité mémoire, etc. {|class="wikitable" |- ! colspan="4" | Micro-opération, encodage en binaire |- | Signaux de commande pour l'ALU | Signaux de commande pour les registres | Signaux de commande pour l'unité d'accès mémoire | Signaux de commande autres |} Toute instruction machine est équivalente à une suite de micro-opérations exécutée dans un ordre précis. Dit autrement, chaque instruction machine est traduite en suite de micro-opérations à chaque fois qu'on l’exécute. C'est le décodeur d'instruction qui transforme une instruction machine en une série de micro-opérations. Pour cela, le décodeur devient un circuit séquentiel. Il est parfois appelé le '''séquenceur''', terme qui trahit bien le fait qu'il traduit une instruction machine en une séquence de micro-opérations. [[File:Micro-operations.svg|centre|vignette|upright=2|Micro-operations]] Certaines µinstructions font un cycle d'horloge, alors que d'autres peuvent prendre plusieurs cycles. Un accès mémoire en RAM peut prendre 200 cycles d'horloge et ne représenter qu'une seule µinstruction, par exemple. Même chose pour certaines opérations de calcul, comme des divisions ou multiplication, qui correspondent à une seule µinstruction mais prennent plusieurs cycles. Typiquement, les instructions des processeurs RISC se font en une seule micro-opération, alors que les instructions complexes des processeurs CISC demandent plusieurs micro-opérations, particulièrement celles qui demandent de faire des accès mémoire ou qui utilisent des modes d'adressage complexes. Ce n'est pas une règle absolue, quelques instructions RISC se font en plusieurs micro-opérations, de même que les processeurs CISC ont des instructions simples qui se font en une seule micro-opération. Mais la corrélation est assez bonne. En conséquence, les processeurs RISC ont des décodeurs d'instruction très simples. ===L'unité de chargement=== [[File:Processeur non-superscalaire basique 01.png|vignette|Processeur basique, avec une unité de chargement.]] L''''unité de chargement''' est placée avant le décodeur/séquenceur. Son rôle est de charger les instructions les unes après les autres, dans le bon ordre. Pour exécuter une suite d'instructions dans le bon ordre, le processeur doit savoir quelle est la prochaine instruction à exécuter : il doit donc contenir une mémoire qui stocke cette information. C'est le rôle du registre d'adresse d'instruction, aussi appelé '''''program counter'''''. L'adresse de la prochaine instruction ne sort pas de nulle part : on peut la déduire de l'adresse de l'instruction en cours d’exécution par divers moyens plus ou moins simples. Généralement, on profite du fait que le programmeur/compilateur place les instructions les unes à la suite des autres en mémoire, dans l'ordre où elles doivent être exécutées. Ainsi, on peut calculer l'adresse de la prochaine instruction en ajoutant la longueur de l'instruction chargée au ''program counter''. Mais sur d'autres processeurs, chaque instruction précise l'adresse de la suivante. Ces processeurs n'ont pas besoin de calculer une adresse qui leur est fournie sur un plateau d'argent. Sur de tels processeurs, chaque instruction précise quelle est la prochaine instruction, directement dans la suite de bit représentant l'instruction en mémoire. Les processeurs de ce type contiennent toujours un registre d'adresse d'instruction, pour faciliter l’interfaçage avec le bus d'adresse. La partie de l'instruction stockant l'adresse de la prochaine instruction est alors recopiée dans ce registre, pour faciliter sa copie sur le bus d'adresse. Mais le compteur ordinal n'existe pas. Sur des processeurs aussi bizarres, pas besoin de stocker les instructions en mémoire dans l'ordre dans lesquelles elles sont censées être exécutées. Mais ces processeurs sont très très rares et peuvent être considérés comme des exceptions à la règle. [[File:Encodage d'une instruction sur un processeur sans Program Counter.png|centre|vignette|upright=2|Encodage d'une instruction sur un processeur sans Program Counter.]] L'unité de chargement s'occupe de tout ce qui a trait au ''program counter'', mais aussi de l'accès mémoire pour charger l'instruction. Il faut noter que sur certains processeurs, le chargement d'une instruction machine est une opération réalisée par le chemin de données. C'est le cas sur les architectures Von Neumann, où il n'y a qu'un seul bus mémoire, qui sert à la fois pour lire/écrire des données et pour charger des instructions. Dans ce cas, l'unité de communication avec la mémoire s'occupe aussi de charger les instructions. Le chargement d'une instruction est alors réalisée par une micro-opération d'accès mémoire, qui copie l'instruction chargée dans un registre dédié, appelé le registre d'instruction, lui-même relié au séquenceur. ===Les autres circuits=== Un processeur contient au minimum une unité de chargement, le chemin de données et le décodeur d'instruction. Mis c'est là le minimum, un processeur peut parfaitement contenir d'autres circuits en plus. Par exemple, il peut intégrer des circuits pour gérer les interruptions matérielles, des circuits ''timer'' pour compter des durées, des circuits dit DMA pour communiquer avec les périphériques, etc. L'exemple du 80186 d'Intel est illustré ci-dessous. Il est composé de deux circuits principaux : une ''Execution Unit'' qui regroupe l'unité de calcul et les registres, une ''Bus Interface Unit'' qui regroupe unité mémoire, unité de chargement et séquenceur. Mais en plus de ces deux circuits principaux, le processeur incorpore des circuits ''timers'', de quoi gérer les interruption, un circuit DMA, et bien d'autres encore. [[File:Intel 80186 80188 arch.svg|centre|vignette|upright=2.5|Intel 80186 80188 arch]] ==Des processeurs vendus en kit aux premiers microprocesseurs== Un processeur est un circuit assez complexe et qui utilise beaucoup de transistors. Avant les années 1970, il n'était pas possible de produire un processeur en un seul morceau. Impossible de mettre un processeur dans un seul boitier. Les tout premiers processeurs étaient fabriqués porte logique par porte logique et comprenaient plusieurs milliers de boitiers reliés entre eux. Par la suite, les progrès de la miniaturisation permirent de faire des pièces plus grandes. L'invention du microprocesseur permis de placer tout le processeur dans un seul boitier, une seule puce électronique. ===Avant l'invention du microprocesseur=== Avant l'invention du microprocesseur, les processeurs étaient fournis en pièces détachées qu'il fallait relier entre elles. Le processeur était composé de plusieurs circuits intégrés, placés sur la même carte mère et connectés ensemble par des fils métalliques. Un exemple de processeur conçu en kit est la série des Intel 3000. Elle regroupe plusieurs circuits séparés : l'Intel 3001 est le séquenceur, l'Intel 3002 est le chemin de données (ALU et registres), le 3003 est un circuit d'anticipation de retenue censé être combiné avec l'ALU, le 3212 est une mémoire tampon, le 3214 est une unité de gestion des interruptions, les 3216/3226 sont des interfaces de bus mémoire. On pourrait aussi citer la famille de circuits intégrés AMD Am2900. Les ALUs en pièces détachées de l'époque étaient assez simples et géraient 2, 4, 8 bits, rarement 16 bits. Et il était possible d'assembler plusieurs ALU pour créer des ALU plus grandes, par exemple combiner plusieurs ALU 4 bits afin de créer une unité de calcul 8 bits, 12 bits, 16 bits, etc. Il s'agit de la méthode du '''''bit slicing''''' que nous avions abordée dans le chapitre sur les unités de calcul. ===L'intel 4004 : le premier microprocesseur=== Par la suite, les progrès de la miniaturisation ont permis de mettre un processeur entier dans un seul circuit intégré. C'est ainsi que sont nés les '''microprocesseurs''', à savoir des processeurs qui tiennent tout entier sur une seule puce de silicium. Les tout premiers microprocesseurs étaient des processeurs à application militaire, comme le processeur du F-14 CADC ou celui de l'''Air data computer''. Le tout premier microprocesseur commercialisé au grand public est le 4004 d'Intel, sorti en 1971. Il comprenait environ 2300 transistors, avait une fréquence de 740 MHz, et manipulait des entiers de 4 bits. De plus, le processeur manipulait des entiers en BCD, ce qui fait qu'il pouvait manipuler un chiffre BCD à la fois (un chiffre BCD est codé sur 4 bits). Il pouvait faire 46 opérations différentes. C'était au départ un processeur de commande, prévu pour être intégré dans la calculatrice Busicom calculator 141-P, mais il fut utilisé pour d'autres applications quelque temps plus tard. Son successeur, l'Intel 4040, garda ces caractéristiques et n'apportait que quelques améliorations mineures : plus de registres, plus d'opérations, etc. Immédiatement après le 4004, les premiers microprocesseurs 8 bits furent commercialisés. Le 4004 fut suivi par le 8008 et quelques autres processeurs 8 bits extrêmement connus, comme le 8080 d'Intel, le 68000 de Motorola, le 6502 ou le Z80. Ces processeurs utilisaient là encore des boitiers similaires au 4004, mais avec plus de broches, vu qu'ils étaient passés de 4 à 8 bits. Par exemple, le 8008 utilisait 18 broches, le 8080 était une version améliorée du 8008 avec 40 broches. Le 8086 fut le premier processeur 16 bits. ===L'évolution des processeurs dans le temps=== La miniaturisation a eu des conséquences notables sur la manière dont sont conçus les processeurs, les mémoires et tous les circuits électroniques en général. On pourrait croire que la miniaturisation a entrainé une augmentation de la complexité des processeurs avec le temps, mais les choses sont à nuancer. Certes, on peut faire beaucoup plus de choses avec un milliard de transistors qu'avec seulement 10000 transistors, ce qui fait que les puces modernes sont d'une certaine manière plus complexes. Mais les anciens processeurs avaient une complexité cachée liée justement au faible nombre de transistors. Il est difficile de concevoir des circuits avec un faible nombre de transistors, ce qui fait que les fabricants de processeurs devaient utiliser des ruses de sioux pour économiser des transistors. Les circuits des processeurs étaient ainsi fortement optimisés pour économiser des portes logiques, à tous les niveaux. Les circuits les plus simples étaient optimisés à mort, on évitait de dupliquer des circuits, on partageait les circuits au maximum, etc. La conception interne de ces processeurs était simple au premier abord, mais avec quelques pointes de complexité dispersées dans toute la puce. De nos jours, les processeurs n'ont plus à économiser du transistor et le résultat est à double tranchant. Certes, ils n'ont plus à utiliser des optimisations pour économiser du circuit, mais ils vont au contraire utiliser leurs transistors pour rendre le processeur plus rapide. Beaucoup des techniques que nous verrons dans ce cours, comme l’exécution dans le désordre, le renommage de registres, les mémoires caches, la présence de plusieurs circuits de calcul, et bien d'autres ; améliorent les performances du processeur en ajoutant des circuits en plus. De plus, on n'hésite plus à dupliquer des circuits qu'on aurait autrefois mis en un seul exemplaire partagé. Tout cela rend le processeur plus complexe à l'intérieur. Une autre contrainte est la facilité de programmation. Les premiers processeurs devaient faciliter au plus la vie du programmeur. Il s'agissait d'une époque où on programmait en assembleur, c'est à dire en utilisant directement les instructions du processeur ! Les processeurs de l'époque utilisaient des jeu d'instruction CISC pour faciliter la vie du programmeur. Pourtant, ils avaient aussi des caractéristiques gênantes pour les programmeurs qui s'expliquent surtout par le faible nombre de transistors de l'époque : peu de registres, registres spécialisés, architectures à pile ou à accumulateur, etc. Ces processeurs étaient assez étranges pour les programmeurs : très simples sur certains points, difficiles pour d'autres. Les processeurs modernes ont d'autres contraintes. Grâce à la grande quantité de transistors dont ils disposent, ils incorporent des caractéristiques qui les rendent plus simples à programmer et à comprendre (registres banalisés, architectures LOAD-STORE, beaucoup de registres, moins d'instructions complexes, autres). De plus, si on ne programme plus les processeurs à la main, les langages de haut niveau passe par des compilateurs qui eux, programment le processeur. Leur interface avec le logiciel a été simplifiée pour coller au mieux avec ce que savent faire les compilateurs. En conséquence, l’interface logicielle des processeurs modernes est paradoxalement plus minimaliste que pour les vieux processeurs. Tout cela pour dire que la conception d'un processeur est une affaire de compromis, comme n'importe quelle tâche d'ingénierie. Il n'y a pas de solution parfaite, pas de solution miracle, juste différentes manières de faire qui collent plus ou moins avec la situation. Et les compromis changent avec l'époque et l'évolution de la technologie. Les technologies sont toutes interdépendantes, chaque évolution concernant les transistors influence la conception des puces électroniques, les technologies architecturales utilisées, ce qui influence l'interface avec le logiciel, ce qui influence ce qu'il est possible de faire en logiciel. Et inversement, les contraintes du logiciel influencent les niveaux les plus bas, et ainsi de suite. Cette morale nous suivra dans le reste du cours, où nous verrons qu'il est souvent possible de résoudre un problème de plusieurs manières différentes, toutes utiles, mais avec des avantages et inconvénients différents. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les interruptions et exceptions | prevText=Les interruptions et exceptions | next=Le chemin de données | nextText=Le chemin de données }} </noinclude> io1qgqe5vglggit7u44lyv0qivfetx4 Les cartes graphiques/L'évolution vers la programmabilité : les GPUs 0 67392 763499 761803 2026-04-11T19:20:53Z Mewtow 31375 /* Les unités de texture sont intégrées aux processeurs de shaders */ 763499 wikitext text/x-wiki Il est intéressant d'étudier le hardware des cartes graphiques en faisant un petit résumé de leur évolution dans le temps. En effet, leur hardware a fortement évolué dans le temps. Et il serait difficile à comprendre le hardware actuel sans parler du hardware d'antan. En effet, une carte graphique moderne est partiellement programmable. Certains circuits sont totalement programmables, d'autres non. Et pour comprendre pourquoi, il faut étudier comment ces circuits ont évolués. Le hardware des cartes graphiques a fortement évolué dans le temps, ce qui n'est pas une surprise. Les évolutions de la technologie, avec la miniaturisation des transistors et l'augmentation de leurs performances a permis aux cartes graphiques d'incorporer de plus en plus de circuits avec les années. Avant l'invention des cartes graphiques, toutes les étapes du pipeline graphique étaient réalisées par le processeur : il calculait l'image à afficher, et l’envoyait à une carte d'affichage 2D. Au fil du temps, de nombreux circuits furent ajoutés, afin de déporter un maximum de calculs vers la carte vidéo. Le rendu 3D moderne est basé sur le placage de texture inverse, avec des coordonnées de texture, une correction de perspective, etc. Mais les anciennes consoles et bornes d'arcade utilisaient le placage de texture direct. Et cela a impacté le hardware des consoles/PCs de l'époque. Avec le placage de texture direct, il était primordial de calculer la géométrie, mais la rasterisation était le fait de VDC améliorés. Aussi, les premières bornes d'arcade 3D et les consoles de 5ème génération disposaient processeurs pour calculer la géométrie et de circuits d'application de textures très particuliers. A l'inverse, les PC utilisaient un rendu inverse, totalement différent. Sur les PC, les premières cartes graphiques avaient un circuit de rastérisation et des unités de textures, mais pas de circuits géométriques. ==Les premières cartes graphiques== Dès les années 70-80, le rendu 3D était utilisé par de nombreuses entreprises industrielles : des applications de visualisation 3D étaient utilisées en architecture, des applications de conception assistée par ordinateur étaient déjà d'utilisation courante, sans compter les simulateurs de vol utilisés par l'armée et les instructeurs qui formaient les pilotes d'avion. Le rendu 3D était aussi étudié au niveau académique, la recherche en 3D était déjà florissante. Il existait même du matériel spécifiquement conçu pour le rendu graphique, mais celui-ci était spécifiquement dédié à des super-calculateurs ou des ''workstations'' (des sortes d'ancêtres des PC, très puissants pour l'époque, mais conçus uniquement pour les entreprises). ===Le début des années 80 : le rendu en fils de fer=== Le tout premier système de ce genre était le '''''Line Drawing System-1''''' de l'entreprise Evans & Sutherland, daté de 1969. Ce n'est ni plus ni moins que le toute premier circuit graphique séparé du processeur ayant existé. C'est en un sens la toute première carte graphique, le tout premier GPU. Il prenait la forme d'un périphérique qui se connectait à l'ordinateur d'un côté et était relié à l'écran de l'autre. Il était compatible avec un grand nombre d'ordinateurs et de processeurs existants. Il a été suivi par plusieurs successeurs, nommés ''Picture System 1, 2'' et le ''PS300 series''. [[File:Evans & Sutherland LDS-1 (1).jpg|vignette|Evans & Sutherland LDS-1 (1)]] Ils permettaient de faire du rendu en fil de fer, sans texture ni même sans polygones colorés. Un tel rendu était utile pour des applications assez limitées : architecture, dessin de molécules pour les entreprises pharmaceutique et certains centres de recherche, l'aérospatiale, etc. Ces cartes graphiques étaient utilisées de concert avec des écrans appelés '''écrans vectoriels''' (''vector display''). Pour simplifier, ils ressemblaient à des écrans CRT, sauf que le faisceau d'électron ne balayait pas l'écran ligne par ligne, mais traçait des lignes arbitraires à l'écran. On lui précisait deux points de coordonnées x1,y1 ; et x2,y2 ; puis l'écran tracait une ligne entre ces deux points. En général, la ligne tracée était maintenue pendant un long moment, entre plusieurs secondes et plusieurs minutes. L'intérieur du circuit était assez simple : un circuit de multiplication de matrice pour les calculs géométriques, un rastériser simplifié (le ''clipping diviser''), un circuit de tracé de lignes, et un processeur de contrôle pour commander les autres circuits. Le fait que ces trois circuits soient séparés permettait une implémentation en pipeline, où plusieurs portions de l'image pouvaient être calculées en même temps : pendant que l'une est dans l'unité géométrique, l'autre est dans le rastériseur et une troisième est en cours de tracé. [[File:Lds1blockdiagram05.svg|centre|vignette|upright=2|Architecture du LDS-1. Le processeur de contrôle n'est pas représenté.]] Le processeur de contrôle exécute un programme qui se charge de commander l'unité géométrique et les autres circuits. Le programme en question est fourni par le programmeur, le LDS-1 est donc totalement programmable. Il lit directement les données nécessaires pour le rendu dans la mémoire de l’ordinateur et le programme exécuté est lui aussi en mémoire principale. Il n'a pas de mémoire vidéo dédiée, il utilise la RAM de l'ordinateur principal. Le multiplieur de matrices est plus complexe qu'on pourrait s'y attendre. Il ne s'agit pas que d'un circuit arithmétique tout simple, mais d'un véritable processeur avec des registres et des instructions machine complexes. Il contient plusieurs registres, l'ensemble mémorisant 4 matrices de 16 nombres chacune (4 lignes de 4 colonnes). Un nombre est codé sur 18 bits. Les registres sont reliés à un ensemble de circuits arithmétiques, des additionneurs et des multiplieurs. Le circuit supporte des instructions de copie entre registres, pour copier une ligne d'une matrice à une autre, des instructions LOAD/STORE pour lire ou écrire dans la mémoire RAM, etc. Il supporte aussi des multiplications en 2D et 3D. Le ''clipping divider'' est un circuit assez complexe, contenant un processeur à accumulateur, une mémoire ROM pour le programme du processeur. Le programme exécuté par le processeur est un petit programme de 62 instructions, stocké dans la ROM. L'algorithme du ''clipping divider'' est décrite dans le papier de recherche "A clipping divider", écrit par Robert Sproull. Un détail assez intéressant est que le résultat en sortie de l'unité géométrique et du rastériseur peuvent être envoyés à l'ordinateur en parallèle du rendu. C'était très utile sur les anciens ordinateurs qui étaient connectés à plusieurs terminaux. Le LDS-1 calculait la géométrie et le rendu, et le tout pouvait petre envoyé à d'autres composants, comme des terminaux, une imprimante, etc. ===Les systèmes ultérieurs : rendu à triangles colorés et texturé=== Les systèmes précédents étaient très limités : ils calculaient la géométrie et n'avaient pas de ''framebuffer'', ni de tampon de profondeur, ni gestion de l'éclairage, ni quoique ce soit. De tels systèmes étaient donc des accélérateurs géométriques que de vrais systèmes graphiques complets, du fait de l'absence de ''framebuffer''. Ils étaient composés de processeurs spécialisés dans les calculs à virgule flottante, faisant des calculs géométriques, et éventuellement d'un processeur pour la rastérisation. La raison est que la RAM était très chère et que créer des circuits fixes étaient très chers et peu disponibles. Par contre, les processeurs à virgule flottante étaient peu chers et facile à trouver. Vers la fin des années 80, grâce à la baisse du prix de la RAM et la démocratisation des ASIC (des circuits fixes fait sur mesure), ajouter un ''framebuffer'' est est devenu possible. C'est alors que sont apparus les '''systèmes de rendu 3D de première génération'''. De tels systèmes ont permis d'implémenter le rendu à primitives colorées qu'on a vu il y a quelques chapitres, à savoir un rendu où les triangles sont coloriés avec une couleur unique. Les systèmes de première génération étaient simples : des processeurs pour le calcul de la géométrie, un circuit de rastérisation, une RAM pour le ''framebuffer'' et des ASIC servant de ROPs très simples. Il n'y avait pas d'élimination des pixels cachés, pas de textures, et encore moins d'éclairage par pixels. Le premier système de ce genre était le ''Shaded Picture System'', toujours par Evans & Sutherland. Il ne gérait pas la couleur et ne pouvait afficher que des images en noir et blanc, mais il gérait l'éclairage par sommet (''vertex lighting''). Il a rapidement été dépassé par les systèmes de l'entreprise ''Silicon Graphics Inc'' (SGI), ainsi que ceux de l'entreprise Apollo avec sa série Apollo DN. Les '''systèmes de seconde génération''' sont apparus vers la fin des années 80, et se distinguent des précédents par l'ajout un tampon de profondeur. Ils intègrent aussi des capacités d'éclairage par pixel, à savoir de l'éclairage plat, de Gouraud, voire de Phong ! Enfin, les '''systèmes de troisième génération''' ont acquis des capacités de placage de texture, que les systèmes précédents n'avaient pas. Ils ont aussi ajouté un support de l'antialiasing. Les systèmes SGI avec placage de texture ont déjà été abordé au chapitre précédent, dans la section sur les GPU en mode immédiat et à ''tile''. Aussi, nous ne reviendrons pas dessus. [[File:Evolution de l'architecture des premières cartes graphiques, dans les années 80-90.png|centre|vignette|upright=2.5|Evolution de l'architecture des premières cartes graphiques, dans les années 80-90]] Les systèmes de première, seconde et troisième génération avaient de nombreux points communs. En premier lieu, ils étaient fabriqués en connectant plusieurs cartes électroniques : une carte pour les calculs géométriques, une ou plusieurs cartes pour le reste du rendu graphique, une carte dédiée au VDC et avec un connecteur écran. Les transistors de l'époque n'étaient pas encore miniaturisés, ce qui fait que le système graphique ne pouvait pas tenir sur une seule carte électronique. Il n'y avait donc pas de carte graphique proprement dit, mais un équivalent éclaté sur plusieurs cartes électroniques. La carte pour la géométrie contenait typiquement une mémoire FIFO pour accumuler les commandes de rendu, un processeur de commande, et plusieurs processeurs géométriques. Les processeurs géométriques étaient parfois conçus sur mesure, comme l'a été le le ''Geometry Engine'' de SGI. Mais il est arrivé qu'ils utilisent des processeurs commerciaux comme le Weitek 3222, l'Intel i860, etc. Les processeurs pouvaient être placés en série ou en parallèle, comme expliqué dans le chapitre précédent. Le circuit de rastérisation était réalisé soit avec un processeur dédié, soit avec un circuit fixe, soit un mélange des deux. La rastérisation est en effet réalisée en plusieurs étapes, certaines peuvent être implémentées avec un processeur et d'autres avec des circuits fixes. Un point important est qu'à l'époque, le rendu n'utilisait pas que des triangles, mais des polygones en général. Ce n'est que par la suite que le rendu s'est focalisé sur les triangles et les ''quads'' (quadrilatères). Il arrivait que le système graphique gérait partiellement des polygones concaves, voire convexes. Sur les systèmes SGI, les calculs géométriques se faisaient avec des polygones, que la rastérisation découpait en triangles, le reste du rendu se faisait avec des triangles. Les stations de travail Apollo DN 10000VS découpaient les polygones en trapézoïdes orientés à l'horizontale, alignés avec des ''scanlines''. D'autres systèmes découpaient tout en triangle lors de l'étape géométrique ==Les précurseurs grand public : les bornes d'arcade== [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] L'accélération du rendu 3D sur les bornes d'arcade était déjà bien avancé dès les années 90. Les bornes d'arcade ont toujours été un segment haut de gamme de l'industrie du jeu vidéo, aussi ce n'est pas étonnant. Le prix d'une borne d'arcade dépassait facilement les 10 000 dollars pour les plus chères et une bonne partie du prix était celui du matériel informatique. Le matériel était donc très puissant et débordait de mémoire RAM comparé aux consoles de jeu et aux PC. La plupart des bornes d'arcade utilisaient du matériel standardisé entre plusieurs bornes. A l'intérieur d'une borne d'arcade se trouve une '''carte de borne d'arcade''' qui est une carte mère avec un ou plusieurs processeurs, de la RAM, une carte graphique, un VDC et pas mal d'autres matériels. La carte est reliée aux périphériques de la borne : joysticks, écran, pédales, le dispositif pour insérer les pièces afin de payer, le système sonore, etc. Le jeu utilisé pour la borne est placé dans une cartouche qui est insérée dans un connecteur spécialisé. Les cartes de bornes d'arcade étaient généralement assez complexes, elles avaient une grande taille et avaient plus de composants que les cartes mères de PC. Chaque carte contenait un grand nombre de chips pour la mémoire RAM et ROM, et il n'était pas rare d'avoir plusieurs processeurs sur une même carte. Et il n'était pas rare d'avoir trois à quatre cartes superposées dans une seule borne. Pour ceux qui veulent en savoir plus, Fabien Sanglard a publié gratuitement un livre sur le fonctionnement des cartes d'arcade CPS System, disponible via ce lien : [https://fabiensanglard.net/b/cpsb.pdf The book of CP System]. Les premières cartes graphiques des bornes d'arcade étaient des cartes graphiques 2D auxquelles on avait ajouté quelques fonctionnalités. Les sprites pouvaient être tournés, agrandit/réduits, ou déformés pour simuler de la perspective et faire de la fausse 3D. Par la suite, le vrai rendu 3D est apparu sur les bornes d'arcade. Dès 1988, la carte d'arcade Namco System 21 et Sega Model 1 géraient les calculs géométriques. Quelques années plus tard, les cartes graphiques se sont mises à supporter un éclairage de Gouraud et du placage de texture. Par exemple, le Namco System 22 et la Sega model 2 supportaient des textures 2D et comme le filtrage de texture (bilinéaire et trilinéaire), le mip-mapping, et quelques autres. Au passage, les cartes graphiques de la Namco System 22 étaient développées en partenariat avec Eans & Sutherland, qui avait commencé à se diversifier dans le marché grand public. Les cartes graphiques de l'époque faisaient les calculs géométriques sur plusieurs processeurs, généralement des processeurs de type DSP (des processeurs spécialisés dans le traitement de signal). Par exemple, la Namco System 2 utilisait 4 DSP de marque Texas Instruments TMS320C25, cadencés à 24,576 MHz. La carte d'arcade Sega Model 1 utilisait quant à elle un DSP spécialisé dans les calculs géométriques. Par la suite, les bornes d'arcade ont réutilisé le hardware des PC et autres consoles de jeux. ==La 3D sur les consoles de quatrième/cinquième génération== Les consoles avant la quatrième génération de console étaient des consoles purement 2D, sans circuits d'accélération 3D. Leur carte graphique était un simple VDC 2D, plus ou moins performant selon la console. Les premières consoles de jeu capables de rendu 3D par elles-mêmes sont les consoles dites de 5ème génération. Il y a diverses manières de classer les consoles en générations, la plus commune place la 3D à la 5ème génération, mais détailler ces controverses quant à ce classement nous amènerait trop loin. Les consoles de génération avaient une architecture assez différente des systèmes antérieurs. Les systèmes SGI et assimilés pouvaient se permettre de couter assez cher, d'utiliser beaucoup de circuits, de prendre beaucoup de place. Les bornes d'arcade sont aussi dans ce cas. Aussi, il n'était pas rare que les cartes 3D de l'époque tiennent sur plusieurs cartes électroniques séparées. Mais une console ne peut pas se permettre ce genre de folies. Aussi, les cartes 3D des consoles de l'époque tenaient dans un seul circuit intégré, comme il est d'usage de nos jours. La conséquence est que certains circuits étaient fortement simplifiés, sur les consoles de cinquième génération. Et cela a impacté l'architecture interne des GPU des consoles. Les systèmes SGI avaient plusieurs processeurs pour calculer la géométrie, couplés à plusieurs unités non-programmables pour les pixels/textures. Les cartes 3D des consoles gardaient cette organisation : processeurs pour la géométrie, circuits fixes pour le reste. Mais elles se débrouillaient souvent avec un seul processeur, voire aucun ! Dans ce dernier cas, la géométrie était calculée sur le processeur principal, le CPU. Les unités pour les pixels étaient aussi moins nombreuses, mais il y en avait plusieurs, pour profiter de l'amplification des pixels. : Les cartes 3D des consoles de jeu utilisaient le placage de texture inverse, avec quelques exceptions qui utilisaient le placage de texture direct. ===Le rendu 3D sur les consoles de quatrième génération : la SNES=== Plus haut, j'ai dit que les consoles de quatrième génération n'avaient pas de carte accélératrice 3D. Pourtant, elles ont connus quelques jeux en vraie 3D. La raison à cela est que la 3D était calculée par un GPU placé dans les cartouches du jeu ! Par exemple, les cartouches de Starfox et de Super Mario 2 contenaient un coprocesseur Super FX, qui gérait des calculs de rendu 2D/3D. En tout, il y a environ 16 coprocesseurs pour la SNES et on en trouve facilement la liste sur le net. La console était conçue pour, des pins sur les ports cartouches étaient prévues pour des fonctionnalités de cartouche annexes, dont ces coprocesseurs. Ces pins connectaient le coprocesseur au bus des entrées-sorties. Les coprocesseurs des cartouches de NES avaient souvent de la mémoire rien que pour eux, qui était intégrée dans la cartouche. Ceci étant dit, passons aux consoles de cinquième génération. ===La Nintendo 64 : un GPU avancé=== La Nintendo 64 avait le GPU le plus complexe comparé aux autres consoles, et dépassait même les cartes graphiques des PC. Il faut dire que son GPU a été conçu avec l'aide de l'entreprise SGI, dont on a vu les systèmes graphiques plus haut. Le GPU de la N64 incorporait une unité pour les calculs géométriques, un circuit de rasterisation, une unité de textures et un ROP final pour les calculs de transparence/brouillard/antialiasing, ainsi qu'un circuit pour gérer la profondeur des pixels. En somme, tout le pipeline graphique était implémenté dans le GPU de la Nintendo 64, chose très en avance sur son temps, comparé au PC ou aux autres consoles ! Le GPU est construit autour d'un processeur dédié aux calculs géométriques, le ''Reality Signal Processor'' (RSP), autour duquel on a ajouté des circuits pour le reste du pipeline graphique. L'unité de calcul géométrique est un processeur MIPS R4000, un processeur assez courant à l'époque, auquel on avait retiré quelques fonctionnalités inutiles pour le rendu 3D. Il était couplé à 4 KB de mémoire vidéo, ainsi qu'à 4 KB de mémoire ROM. Le reste du GPU était réalisé avec des circuits fixes. Un point intéressant est que le programme exécuté par le RSP pouvait être programmé ! Le RSP gérait déjà des espèces de proto-shaders, qui étaient appelés des ''[https://ultra64.ca/files/documentation/online-manuals/functions_reference_manual_2.0i/ucode/microcode.html micro-codes]'' dans la documentation de l'époque. La ROM associée au RSP mémorise cinq à sept programmes différents, aux fonctionnalités différentes. * Les microcodes gspFast3D et gspF3DNoN, implémentent un rendu 3D normal, avec des options de ''clipping'' différentes entre les deux. * Le microcode gspTurbo3D fait la même chose, mais avec moins de fonctionnalités et avec une précision réduite. Il ne gère pas le ''clipping'', l'éclairage par pixel, la correction de perspective, l'antialiasing et quelques autres fonctionnalités. Il gère cependant l'éclairage de Gouraud. Il utilise une ''display list'' simplifiée comparé aux deux microcodes précédents. * Le microcode gspZ-Sort effectue une pré-passe z, à savoir qu'il calcule le tampon de profondeur final de la scène 3D, sans rendre l'image. Cela sert à faire une élimination des pixels cachés parfaite, en logiciel. On calcule le tampon de profondeur pour déterminer quels pixels sont visibles, puis une seconde passe rend l'image en, rejetant les pixels non-visibles. * Le microcode gspSprite2D implémente un rendu 2D émulé : les sprites et arrière-plan sont des rectangles texturés. Le microcode gspS2DEX fait la même chose, mais sert à émuler le rendu de la SNES plus qu'autre chose. * Le microcode gspLine3D ne gére que des lignes, pas de triangles. Il sert pour du rendu en fil de fer. Ils géraient le rendu 3D de manière différente et avec une gestion des ressources différentes. Très peu de studios de jeu vidéo ont développé leur propre microcodes N64, car la documentation était mal faite, que Nintendo ne fournissait pas de support officiel pour cela, que les outils de développement ne permettaient pas de faire cela proprement et efficacement. ===La Playstation 1=== Sur la Playstation 1 le calcul de la géométrie était réalisé par le processeur, la carte graphique gérait tout le reste. Et la carte graphique était un circuit fixe spécialisé dans la rasterisation et le placage de textures. Elle utilisait, comme la Nintendo 64, le placage de texture inverse, qui est apparu ensuite sur les cartes graphiques. ===La 3DO et la Sega Saturn=== La Sega Saturn et la 3DO étaient les deux seules consoles à utiliser le rendu direct. La géométrie était calculée sur le processeur, même si les consoles utilisaient parfois un CPU dédié au calcul de la géométrie. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. La Sega Saturn incorpore trois processeurs et deux GPU. Les deux GPUs sont nommés le VDP1 et le VDP2. Le VDP1 s'occupe des textures et des sprites, le VDP2 s'occupe uniquement de l'arrière-plan et incorpore un VDC tout ce qu'il y a de plus simple. Ils ne gèrent pas du tout la géométrie, qui est calculée par les trois processeurs. Le troisième processeur, la Saturn Control Unit, est un processeur de type DSP, à savoir un processeur spécialisé dans le traitement de signal. Il est utilisé presque exclusivement pour accélérer les calculs géométriques. Il avait sa propre mémoire RAM dédiée, 32 KB de SRAM, soit une mémoire locale très rapide. Les transferts entre cette RAM et le reste de l'ordinateur était géré par un contrôleur DMA intégré dans le DSP. En somme, il s'agit d'une sorte de processeur spécialisé dans la géométrie, une sorte d'unité géométrique programmable. Mais la géométrie n'était pas forcément calculée que sur ce DSP, mais pouvait être prise en charge par les 3 CPU. ==L'historique des cartes graphiques des PC, avant l'arrivée de Direct X et Open Gl== Sur PC, l'évolution des cartes graphiques a eu du retard par rapport aux consoles. Les PC sont en effet des machines multi-usage, pour lesquelles le jeu vidéo était un cas d'utilisation parmi tant d'autres. Et les consoles étaient la plateforme principale pour jouer à des jeux vidéo, le jeu vidéo PC étant plus marginal. Mais cela ne veut pas dire que le jeu PC n'existait pas, loin de là ! Un problème pour les jeux PC était que l'écosystème des PC était aussi fragmenté en plusieurs machines différentes : machines Apple 1 et 2, ordinateurs Commdore et Amiga, IBM PC et dérivés, etc. Aussi, programmer des jeux PC n'était pas mince affaire, car les problèmes de compatibilité étaient légion. C'est seulement quand la plateforme x86 des IBM PC s'est démocratisée que l'informatique grand public s'est standardisée, réduisant fortement les problèmes de compatibilité. Mais cela n'a pas suffit, il a aussi fallu que les API 3D naissent. Les API 3D comme Direct X et Open GL sont absolument cruciales pour garantir la compatibilité entre plusieurs ordinateurs aux cartes graphiques différentes. Aussi, l'évolution des cartes graphiques pour PC s'est faite main dans la main avec l'évolution des API 3D. Les fonctionnalités des cartes graphiques ont évolué dans le temps, en suivant les évolutions des API 3D. Du moins dans les grandes lignes, car il est arrivé plusieurs fois que des fonctionnalités naissent sur les cartes graphiques, pour que les fabricants forcent la main de Microsoft ou d'Open GL pour les intégrer de force dans les API 3D. Passons. ===L'introduction des premiers jeux 3D : Quake et les drivers miniGL=== L'API OpenGL est née de la main de SGI, encore eux ! SGI avait créé l'API Iris GL pour ses stations de travail Iris Graphics. Iris GL a ensuite été libéré et est devenu le standard Open GL. Open GL existait déjà avant l'apparition des cartes accélératrices 3D. Il y a avait donc déjà un terreau que les programmeurs graphiques pouvaient utiliser. Mais Open GL était surtout utilisé pour des applications industrielles, médicales (imagerie), graphiques ou militaires, pas pour le jeu vidéo. Mais cela changea avec la sortie du jeu Quake, d'IdSoftware, en 1996. Quake pouvait fonctionner en rendu logiciel, mais le programmeur responsable du moteur 3D (le célébre John Carmack) ajouta une version OpenGL du jeu. Il faut dire que le jeu était programmé sur une station de travail compatible avec OpenGL, même si aucune carte accélératrice de l'époque ne supportait OpenGL. C'était là un choix qui se révéla visionnaire. En théorie, le rendu par OpenGL aurait dû se faire intégralement en logiciel, sauf sur quelques rares stations de travail adaptées. Mais les premières cartes graphiques étaient déjà dans les starting blocks. La toute première carte 3D pour PC est la '''Rendition Vérité V1000''', sortie en Septembre 1995, soit quelques mois avant l'arrivée de la Nintendo 64. La Rendition Vérité V1000 contenait un processeur MIPS cadencé à 25 MHz, 4 mébioctets de RAM, une ROM pour le BIOS, et un RAMDAC, rien de plus. C'était un vrai ordinateur complètement programmable de bout en bout, sans aucun circuit fixe. Les programmeurs ne pouvaient cependant pas utiliser cette programmabilité avec des ''shaders'', mais elle permettait à Rendition d'implémenter n'importe quelle API 3D, que ce soit OpenGL, DirectX ou même sa son API propriétaire. La Rendition Vérité avait de bonnes performances pour ce qui est de la géométrie, mais pas pour le reste. Réaliser la rastérisation et le placage de texture en logiciel n'est pas efficace, pareil pour les opérations de fin de pipeline comme l'antialiasing. Le manque d'unités fixes très rapides pour la rastérisation, le placage de texture ou les opérations de fin de pipeline était clairement un gros défaut. Mais la Rendition Vérité était un cas à part, une exception dans le paysage des cartes 3D de l'époque, qui ne faisait rien comme les autres. Les autres cartes graphiques, sorties peu après, étaient les Voodoo de 3dfx, les Riva TNT de NVIDIA, les Rage/3D d'ATI, la Virge/3D de S3, et la Matrox Mystique. Elles avaient choisit le compromis inverse de la Rendition Vérité V1000 : de bonnes performances pour le placage de textures et la rastérization, mais pas pour les calculs géométriques. Pour rappel, les systèmes professionnels et les consoles avaient des processeurs pour la géométrie, et des circuits fixes pour le reste. Les cartes graphiques de PC se passaient des processeurs pour la géométrie, les calculs géométriques étaient réalisés par le CPU. Les toutes premières cartes 3D pour PC contenaient seulement des circuits pour gérer les textures et des ROPs. Elle géraient le ''z-buffer'' en mémoire vidéo, ainsi que des effets de brouillard. Il n'y avait même pas de circuit pour la rastérisation, qui était faite en logiciel, avec les calculs géométriques. [[File:Architecture de base d'une carte 3D - 2.png|centre|vignette|upright=1.5|Carte 3D sans rasterization matérielle.]] Les cartes suivantes ajoutèrent une gestion des étapes de ''rasterization'' directement en matériel. Les cartes ATI rage 2, les Invention de chez Rendition, et d'autres cartes graphiques supportaient la rasterisation en hardware. [[File:Architecture de base d'une carte 3D - 3.png|centre|vignette|upright=1.5|Carte 3D avec gestion de la géométrie.]] Pour exploiter les unités de texture et le circuit de rastérisation, OpenGL et Direct 3D étaient partiellement implémentées en logiciel, car les cartes graphiques ne supportaient pas toutes les fonctionnalités de l'API. C'était l'époque du miniGL, des implémentations partielles d'OpenGL, fournies par les fabricants de cartes 3D, implémentées dans les pilotes de périphériques de ces dernières. Les fonctionnalités d'OpenGL implémentées dans ces pilotes étaient presque toutes exécutées en matériel, par la carte graphique. Avec l'évolution du matériel, les pilotes de périphériques devinrent de plus en plus complets, au point de devenir des implémentations totales d'OpenGL. Mais au-delà d'OpenGL, chaque fabricant de carte graphique avait sa propre API propriétaire, qui était gérée par leurs pilotes de périphériques (''drivers''). Par exemple, les premières cartes graphiques de 3dfx interactive, les fameuses voodoo, disposaient de leur propre API graphique, l'API Glide. Elle facilitait la gestion de la géométrie et des textures, ce qui collait bien avec l'architecture de ces cartes 3D. Mais ces API propriétaires tombèrent rapidement en désuétude avec l'évolution de DirectX et d'OpenGL. Direct X était une API dans l'ombre d'Open GL. La première version de Direct X qui supportait la 3D était DirectX 2.0 (juin 2, 1996), suivie rapidement par DirectX 3.0 (septembre 1996). Elles dataient d'avant le jeu Quake, et elles étaient très éloignées du hardware des premières cartes graphiques. Elles utilisaient un système d'''execute buffer'' pour communiquer avec la carte graphique, Microsoft espérait que le matériel 3D implémenterait ce genre de système. Ce qui ne fu pas le cas. Direct X 4.0 a été abandonné en cours de développement pour laisser à une version 5.0 assez semblable à la 2.0/3.0. Le mode de rendu laissait de côté les ''execute buffer'' pour coller un peu plus au hardware de l'époque. Mais rien de vraiment probant comparé à Open GL. Même Windows utilisait Open GL au lieu de Direct X maison... C'est avec Direct X 6.0 que Direct X est entré dans la cours des grands. Il gérait la plupart des technologies supportées par les cartes graphiques de l'époque. ===Le ''multi-texturing'' de l'époque Direct X 6.0 : combiner plusieurs textures=== Une technologie très importante standardisée par Dirext X 6 est la technique du '''''multi-texturing'''''. Avec ce qu'on a dit dans le chapitre précédent, vous pensez sans doute qu'il n'y a qu'une seule texture par objet, qui est plaquée sur sa surface. Mais divers effet graphiques demandent d'ajouter des textures par dessus d'autres textures. En général, elles servent pour ajouter des détails, du relief, sur une surface pré-existante. Un exemple intéressant vient des jeux de tir : ajouter des impacts de balles sur les murs. Pour cela, on plaque une texture d'impact de balle sur le mur, à la position du tir. Il s'agit là d'un exemple de '''''decals''''', des petites textures ajoutées sur les murs ou le sol, afin de simuler de la poussière, des impacts de balle, des craquelures, des fissures, des trous, etc. Les textures en question sont de petite taille et se superposent à une texture existante, plus grande. Rendre des ''decals'' demande de pouvoir superposer deux textures. Direct X 6.0 supportait l'application de plusieurs textures directement dans le matériel. La carte graphique devait être capable d'accéder à deux textures en même temps, ou du moins faire semblant que. Pour cela, elle doublaient les unités de texture et adaptaient les connexions entre unités de texture et mémoire vidéo. La mémoire vidéo devait être capable de gérer plusieurs accès mémoire en même temps et devait alors avoir un débit binaire élevé. [[File:Multitexturing.png|centre|vignette|upright=2|Multitexturing]] La carte graphique devait aussi gérer de quoi combiner deux textures entre elles. Par exemple, pour revenir sur l'exemple d'une texture d'impact de balle, il faut que la texture d'impact recouvre totalement la texture du mur. Dans ce cas, la combinaison est simple : la première texture remplace l'ancienne, là où elle est appliquée. Mais les cartes graphiques ont ajouté d'autres combinaisons possibles, par exemple additionner les deux textures entre elle, faire une moyenne des texels, etc. Les opérations pour combiner les textures était le fait de circuits appelés des '''''combiners'''''. Concrètement, les ''combiners'' sont de simples unités de calcul. Les ''conbiners'' ont beaucoup évolués dans le temps, mais les premières implémentation se limitaient à quelques opérations simples : addition, multiplication, superposition, interpolation. L'opération effectuer était envoyée au ''conbiner'' sur une entrée dédiée. [[File:Multitexturing avec combiners.png|centre|vignette|upright=2|Multitexturing avec combiners]] S'il y avait eu un seul ''conbiner'', le circuit de ''multitexturing'' aurait été simplement configurable. Mais dans la réalité, les premières cartes utilisant du ''multi-texturing'' utilisaient plusieurs ''combiners'' placés les uns à la suite des autres. L'implémentation des ''combiners'' retenue par Open Gl, et par le hardware des cartes graphiques, était la suivante. Les ''combiners'' étaient placés en série, l'un à la suite de l'autre, chacun combinant le résultat de l'étage précédent avec une texture. Le premier ''combiner'' gérait l'éclairage par sommet, afin de conserver un minimum de rétrocompatibilité. [[File:Texture combiners Open GL.png|centre|vignette|upright=2|Texture combiners Open GL]] Voici les opérations supportées par les ''combiners'' d'Open GL. Ils prennent en entrée le résultat de l'étage précédent et le combinent avec une texture lue depuis l'unité de texture. {|class="wikitable" |+ Opérations supportées par les ''combiners'' d'Open GL |- ! Replace | colspan="2" | Pixel provenant de l'unité de texture |- ! Addition | colspan="2" | Additionne l'entrée au texel lu. |- ! Modulate | colspan="2" | Multiplie l'entrée avec le texel lu |- ! Mélange (''blending'') | Moyenne pondérée des deux entrées, pondérée par la composante de transparence || La couleur de transparence du texel lu et de l'entrée sont multipliées. |- ! Decals | Moyenne pondérée des deux entrées, pondérée par la composante de transparence. || La transparence du résultat est celle de l'entrée. |} Il faut noter qu'un dernier étage de ''combiners'' s'occupait d'ajouter la couleur spéculaire et les effets de brouillards. Il était à part des autres et n'était pas configurable, c'était un étage fixe, qui était toujours présent, peu importe le nombre de textures utilisé. Il était parfois appelé le '''''combiner'' final''', terme que nous réutiliserons par la suite. Mine de rien, cela a rendu les cartes graphiques partiellement programmables. Le fait qu'il y ait des opérations enchainées à la suite, opérations qu'on peut choisir librement, suffit à créer une sorte de mini-programme qui décide comment mélanger plusieurs textures. Mais il y avait une limitation de taille : le fait que les données soient transmises d'un étage à l'autre, sans détours possibles. Par exemple, le troisième étage ne pouvait avoir comme seule opérande le résultat du second étage, mais ne pouvait pas utiliser celui du premier étage. Il n'y avait pas de registres pour stocker ce qui sortait de la rastérisation, ni pour mémoriser temporairement les texels lus. ===Le ''Transform & Lighting'' matériel de Direct X 7.0=== [[File:Architecture de base d'une carte 3D - 4.png|vignette|upright=1.5|Carte 3D avec gestion de la géométrie.]] La première carte graphique pour PC capable de gérer la géométrie en hardware fût la Geforce 256, la toute première Geforce. Son unité de gestion de la géométrie n'est autre que la bien connue '''unité T&L''' (''Transform And Lighting''). Elle implémentait des algorithmes d'éclairage de la scène 3D assez simples, comme un éclairage de Gouraud, qui étaient directement câblés dans ses circuits. Mais contrairement à la Nintendo 64 et aux bornes d'arcade, elle implémentait le tout, non pas avec un processeur classique, mais avec des circuits fixes. Avec Direct X 7.0 et Open GL 1.0, l'éclairage était en théorie limité à de l'éclairage par sommet, l'éclairage par pixel n'était pas implémentable en hardware. Les cartes graphiques ont tenté d'implémenter l'éclairage par pixel, mais cela n'est pas allé au-delà du support de quelques techniques de ''bump-mapping'' très limitées. Par exemple, Direct X 6.0 implémentait une forme limitée de ''bump-mapping'', guère plus. Un autre problème est qu'il a beaucoup d'algorithmes d'éclairages différents, aux résultats visuels différents, bien au-delà des algorithmes d'éclairage plat, de Gouraud et de Phong. Et les unités de T&L étaient souvent en retard sur les algorithmes logiciels. Les programmeurs avaient le choix entre programmer les algorithmes d’éclairage qu'ils voulaient et les exécuter en logiciel, ou utiliser ceux de l'unité de T&L. Ils choisissaient souvent la première option. Par exemple, Quake 3 Arena et Unreal Tournament n'utilisaient pas les capacités d'éclairage géométrique et préféraient utiliser leurs calculs d'éclairage logiciel fait maison. Cependant, le hardware dépassait les capacités des API et avait déjà commencé à ajouter des capacités de programmation liées au ''multi-texturing''. Les cartes graphiques de l'époque, surtout chez NVIDIA, implémentaient un système de '''''register combiners''''', une forme améliorée de ''texture combiners'', qui permettait de faire une forme limitée d'éclairage par pixel, notamment du vrai ''bump-mampping'', voire du ''normal-mapping''. Mais ce n'était pas totalement supporté par les API 3D de l'époque. Les ''registers combiners'' sont des ''texture combiners'' mais dans lesquels ont aurait retiré la stricte organisation en série. Il y a toujours plusieurs étages à la suite, qui peuvent exécuter chacun une opération, mais tous les étages ont maintenant accès à toutes les textures lues et à tout ce qui sort de la rastérisation, pas seulement au résultat de l'étape précédente. Pour cela, on ajoute des registres pour mémoriser ce qui sort des unités de texture, et pour ce qui sort de la rastérisation. De plus, on ajoute des registres temporaires pour mémoriser les résultats de chaque ''combiner'', de chaque étage. Il faut cependant signaler qu'il existe un ''combiner'' final, séparé des étages qui effectuent des opérations proprement dits. Il s'agit de l'étage qui applique la couleur spéculaire et les effets de brouillards. Il ne peut être utilisé qu'à la toute fin du traitement, en tant que dernier étage, on ne peut pas mettre d'opérations après lui. Sa sortie est directement connectée aux ROPs, pas à des registres. Il faut donc faire la distinction entre les '''''combiners'' généraux''' qui effectuent une opération et mémorisent le résultat dans des registres, et le ''combiner'' final qui envoie le résultat aux ROPs. L'implémentation des ''register combiners'' utilisait un processeur spécialisés dans les traitements sur des pixels, une sorte de proto-processeur de ''shader''. Le processeur supportait des opérations assez complexes : multiplication, produit scalaire, additions. Il s'agissait d'un processeur de type VLIW, qui sera décrit dans quelques chapitres. Mais ce processeur avait des programmes très courts. Les premières cartes NVIDIA, comme les cartes TNT pouvaient exécuter deux opérations à la suite, suivie par l'application de la couleurs spéculaire et du brouillard. En somme, elles étaient limitées à un ''shader'' à deux/trois opérations, mais c'était un début. Le nombre d'opérations consécutives est rapidement passé à 8 sur la Geforce 3. ===L'arrivée des ''shaders'' avec Direct X 8.0=== [[File:Architecture de la Geforce 3.png|vignette|upright=1.5|Architecture de la Geforce 3]] Les ''register combiners'' était un premier pas vers un éclairage programmable. Paradoxalement, l'évolution suivante s'est faite non pas dans l'unité de rastérisation/texture, mais dans l'unité de traitement de la géométrie. La Geforce 3 a remplacé l'unité de T&L par un processeur capable d'exécuter des programmes. Les programmes en question complétaient l'unité de T&L, afin de pouvoir rajouter des techniques d'éclairage plus complexes. Le tout a permis aussi d'ajouter des animations, des effets de fourrures, des ombres par ''shadow volume'', des systèmes de particule évolués, et bien d'autres. À partir de la Geforce 3 de Nvidia, les cartes graphiques sont devenues capables d'exécuter des programmes appelés '''''shaders'''''. Le terme ''shader'' vient de ''shading'' : ombrage en anglais. Grace aux ''shaders'', l'éclairage est devenu programmable, il n'est plus géré par des unités d'éclairage fixes mais été laissé à la créativité des programmeurs. Les programmeurs ne sont plus vraiment limités par les algorithmes d'éclairage implémentés dans les cartes graphiques, mais peuvent implémenter les algorithmes d'éclairage qu'ils veulent et peuvent le faire exécuter directement sur la carte graphique. Les ''shaders'' sont classifiés suivant les données qu'ils manipulent : '''''pixel shader''''' pour ceux qui manipulent des pixels, '''''vertex shaders''''' pour ceux qui manipulent des sommets. Les premiers sont utilisés pour implémenter l'éclairage par pixel, les autres pour gérer tout ce qui a trait à la géométrie, pas seulement l'éclairage par sommets. Direct X 8.0 avait un standard pour les shaders, appelé ''shaders 1.0'', qui correspondait parfaitement à ce dont était capable la Geforce 3. Il standardisait les ''vertex shaders'' de la Geforce 3, mais il a aussi renommé les ''register combiners'' comme étant des ''pixel shaders'' version 1.0. Les ''register combiners'' n'ont pas évolués depuis la Geforce 256, si ce n'est que les programmes sont passés de deux opérations successives à 8, et qu'il y avait possibilité de lire 4 textures en ''multitexturing''. A l'opposé, le processeur de ''vertex shader'' de la Geforce 3 était capable d'exécuter des programmes de 128 opérations consécutives et avait 258 registres différents ! Des ''pixels shaders'' plus évolués sont arrivés avec l'ATI Radeon 8500 et ses dérivés. Elle incorporait la technologie ''SMARTSHADER'' qui remplacait les ''registers combiners'' par un processeur de ''shader'' un peu limité. Un point est que le processeur acceptait de calculer des adresses de texture dans le ''pixel shader''. Avant, les adresses des texels à lire étaient fournis par l'unité de rastérisation et basta. L'avantage est que certains effets graphiques étaient devenus possibles : du ''bump-mapping'' avancé, des textures procédurales, de l'éclairage par pixel anisotrope, du éclairage de Phong réel, etc. Avec la Radeon 8500, le ''pixel shader'' pouvait calculer des adresses, et lire les texels associés à ces adresses calculées. Les ''pixel shaders'' pouvaient lire 6 textures, faire 8 opérations sur les texels lus, puis lire 6 textures avec les adresses calculées à l'étape précédente, et refaire 8 opérations. Quelque chose de limité, donc, mais déjà plus pratique. Les ''pixel shaders'' de ce type ont été standardisé dans Direct X 8.1, sous le nom de ''pixel shaders 1.4''. Encore une fois, le hardware a forcé l'intégration dans une API 3D. ===Les ''shaders'' de Direct X 9.0 : de vrais ''pixel shaders''=== Avec Direct X 9.0, les ''shaders'' sont devenus de vrais programmes, sans les limitations des ''shaders'' précédents. Les ''pixels shaders'' sont passés à la version 2.0, idem pour les ''vertex shaders''. Concrètement, ils ont des fonctionnalités bien supérieures à celles des ''registers combiners''. Les ''shaders'' pouvaient exécuter une suite d'opérations arbitraire, dans le sens où elle n'était pas structurée avec tel type d'opération au début, suivie par un accès aux textures, etc. On pouvait mettre n'importe quelle opération dans n'importe quel ordre. De plus, les ''shaders'' ne sont plus écrit en assembleur comme c'était le cas avant. Ils sont dorénavant écrits dans un langage de haut-niveau, le HLSL pour les shaders Direct X et le GLSL pour les shaders Open Gl. Les ''shaders'' sont ensuite traduit (compilés) en instructions machines compréhensibles par la carte graphique. Au début, ces langages et la carte graphique supportaient uniquement des opérations simples. Mais au fil du temps, les spécifications de ces langages sont devenues de plus en plus riches à chaque version de Direct X ou d'Open Gl, et le matériel en a fait autant. Le matériel s'est alors adapté, en incorporant un véritable processeur pour les ''pixel shaders''. Les ''pixel shaders'' sont maintenant exécutés par un processeur de ''shader'' dédié, aux fonctionnalités bien supérieures à celles des ''registers combiners''. Le processeur de ''pixel shader'' incorpore l'unité de texture en sont sein, les deux sont fusionnés. La raison à cela sera expliqué dans la suite du chapitre. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=1.5|Carte 3D avec pixels et vertex shaders non-unifiés.]] ===L'après Direct X 9.0=== Avant Direct X 10, les processeurs de ''shaders'' ne géraient pas exactement les mêmes opérations pour les processeurs de ''vertex shader'' et de ''pixel shader''. Les processeurs de ''vertex shader'' et de ''pixel shader''étaient séparés. Depuis DirectX 10, ce n'est plus le cas : le jeu d'instructions a été unifié entre les vertex shaders et les pixels shaders, ce qui fait qu'il n'y a plus de distinction entre processeurs de vertex shaders et de pixels shaders, chaque processeur pouvant traiter indifféremment l'un ou l'autre. [[File:Architecture de base d'une carte 3D - 6.png|centre|vignette|upright=1.5|Architecture de la GeForce 6800.]] Avec Direct X 10, de nombreux autres ''shaders'' sont apparus. Les plus liés au rendu 3D sont les '''''geometry shader''''' pour ceux qui manipulent des triangles, de ''hull shaders'' et de ''domain shaders'' pour la tesselation. De plus, les cartes graphiques modernes sont capables d’exécuter des programmes informatiques qui n'ont aucun lien avec le rendu 3D, mais sont exécutés par la carte graphique comme le ferait un processeur d'ordinateur normal. De tels ''shaders'' sans lien avec le rendu 3D sont appelés des ''compute shader''. ==Les cartes graphiques d'aujourd'hui== Avec l'arrivée des shaders, les circuits d'une carte graphique sont divisés en deux catégories : d'un côté les circuits non-programmables et de l'autre les circuits programmables. Pour exécuter les ''shaders'', la carte graphique incorpore des '''processeurs de ''shaders''''', des processeurs similaires aux processeurs des ordinateurs, aux CPU, mais avec quelques petites différences qu'on expliquera dans le prochain chapitre. A côté des processeurs de ''shaders'', il reste quelques circuits non(programmables appelés des circuits fixes. De nos jours, la gestion de la géométrie et des pixels est programmable, mais la rastérisation, le placage de texture, le ''culling'' et l'enregistrement du ''framebuffer'' ne l'est pas. Il n'en a pas toujours été ainsi. [[File:3D-Pipeline.svg|centre|vignette|upright=3.0|Pipeline 3D : ce qui est programmable et ce qui ne l'est pas dans une carte graphique moderne.]] ===Les GPU modernes sont un mélange de processeurs et de circuits fixes=== Une carte graphique contient donc un mélange de circuits fixes et de processeurs de ''shaders'', qui peut sembler contradictoire. Pourquoi ne pas tout rendre programmable ? Ou au contraire, utiliser seulement des circuits fixes ? La réponse rapide est qu'il s'agit d'un compromis entre flexibilité et performance qui permet d'avoir le meilleur des deux mondes. Mais ce compromis a fortement évolué dans le temps, comme on va le voir plus bas. Rendre la gestion de la géométrie ou des pixels programmable permet d'implémenter facilement un grand nombre d'effets graphiques sans avoir à les implémenter en hardware. Avant les ''shaders'', seul le hardware récent gérait les dernières fonctionnalités. Les effets graphiques derniers cri n'étaient disponibles que sur les derniers modèles de carte graphique. Avec des ''vertex/pixel shaders'', ce genre de défaut est passé à la trappe. Si un nouvel algorithme de rendu graphique est inventé, il peut être utilisé dès le lendemain sur toutes les cartes graphiques modernes. De plus, implémenter beaucoup d'algorithmes d'éclairage différents est difficile. Le cout en termes de transistors et de complexité était assez important, utiliser des circuits programmable a un cout en hardware plus limité. Tout cela est à l'exact opposé de ce qu'on a avec les autres circuits, comme les circuits pour la rastérisation ou le placage de texture. Il n'y a pas 36 façons de rastériser une scène 3D et la flexibilité n'est pas un besoin important pour cette opération, alors que les performances sont cruciales. Même chose pour le placage/filtrage de textures. En conséquences, les unités de transformation, de rastérisation et de placage de texture sont toutes implémentées en matériel. Faire ainsi permet de gagner en performance sans que cela ait le moindre impact pour le programmeur. ===Les unités de texture sont intégrées aux processeurs de shaders=== Avec l'arrivée des processeurs de shaders, les unités de texture ont été intégrées dans les processeurs de shaders eux-mêmes. C'est la seule unité fixe qui a subit ce traitement, et il est intéressant de comprendre pourquoi. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=2|Architecture de base d'une carte 3D.]] Pour cela, il faut faire un rappel sur ce qu'il y a dans un processeur. Un processeur contient globalement quatre circuits : * une unité de calcul qui fait des calculs ; * des registres pour stocker les opérandes et résultats des calculs ; * une unité de communication avec la mémoire ; * et un séquenceur, un circuit de contrôle qui commande les autres. L'unité de communication avec la mémoire sert à lire ou écrire des données, à les transférer de la RAM vers les registres, ou l'inverse. Lire une donnée demande d'envoyer son adresse à la RAM, qui répond en envoyant la donnée lue. Elle est donc toute indiquée pour lire une texture : lire une texture n'est qu'un cas particulier de lecture de données. Les texels à lire sont à une adresse précise, la RAM répond à la lecture avec le texel demandé. Il est donc possible d'utiliser l'unité de communication avec la mémoire comme si c'était une unité de texture. Cependant, les textures ne sont pas utilisées comme telles de nos jours. Le rendu 3D moderne utilise des techniques dites de filtrage de texture, qui permettent d'améliorer la qualité du rendu des textures. Sans ce filtrage de texture, les textures appliquées naïvement donnent un résultat assez pixelisé et assez moche, pour des raisons assez techniques. Le filtrage élimine ces artefacts, en utilisant une forme d'''antialiasing'' interne aux textures, le fameux filtrage de texture. Le filtrage de texture peut être réalisé en logiciel ou en matériel. Techniquement, il est possible de le faire dans un ''shader''. Le ''shader'' calcule les adresses des texels à lire, lit les texels, et effectue ensuite le filtrage avec des opérations de calcul. Mais ce n'est pas ce qui est fait, le filtrage de texture est toujours effectué directement en matériel. La raison est que le filtrage de texture est très simple à implémenter en hardware. Le filtrage bilinéaire ou trilinéaire demande juste des circuits d'interpolation et quelques registres, ce qui est trivial. Et la seconde raison est qu'il n'y a pas 36 façons de filtrer des textures : une carte graphique peut implémenter les algorithmes principaux existants en assez peu de circuits. Pour simplifier l'implémentation, les processeurs de ''shader'' modernes disposent d'une unité d'accès mémoire séparée de l'unité de texture. L'unité d'accès mémoire normale s'occupe des accès mémoire hors-textures, alors que l'unité mémoire s'occupe de lire les textures. L'unité de texture contient de quoi faire du filtrage de texture, mais aussi faire des calculs d'adresse spécialisées, intrinsèquement liés au format des textures, qu'on détaillera dans le chapitre sur les textures. En comparaison, les unités d'accès mémoire effectuent des calculs d'adresse plus basiques. Un dernier avantage est que l'unité de texture est reliée au cache de texture, alors que l'unité d'accès mémoire est relié au cache L1/L2. : Il faut noter que les ROPs peuvent aussi être intégré dans les processeurs de ''shader'', mais c'est assez rare. Les cartes graphiques pour PC ont des ROPs séparés des processeurs de ''shaders''. Par contre, quelques cartes graphiques destinées les smartphones et autres appareils mobiles font exception. Sur celles-ci, les unités de textures et les ROPs sont intégrés dans les processeurs de ''shaders'', aux côtés de l'unité d'accès mémoire LOAD/STORE. Il s'agit de cartes graphiques en rendu à tuiles. {{NavChapitre | book=Les cartes graphiques | prev=Les cartes graphiques : architecture de base | prevText=Les cartes graphiques : architecture de base | next=Les processeurs de shaders | nextText=Les processeurs de shaders }} {{autocat}} 58xxfjhi9xdqp71i18lp9o41s509ukc 763500 763499 2026-04-11T19:21:10Z Mewtow 31375 /* Les cartes graphiques d'aujourd'hui */ 763500 wikitext text/x-wiki Il est intéressant d'étudier le hardware des cartes graphiques en faisant un petit résumé de leur évolution dans le temps. En effet, leur hardware a fortement évolué dans le temps. Et il serait difficile à comprendre le hardware actuel sans parler du hardware d'antan. En effet, une carte graphique moderne est partiellement programmable. Certains circuits sont totalement programmables, d'autres non. Et pour comprendre pourquoi, il faut étudier comment ces circuits ont évolués. Le hardware des cartes graphiques a fortement évolué dans le temps, ce qui n'est pas une surprise. Les évolutions de la technologie, avec la miniaturisation des transistors et l'augmentation de leurs performances a permis aux cartes graphiques d'incorporer de plus en plus de circuits avec les années. Avant l'invention des cartes graphiques, toutes les étapes du pipeline graphique étaient réalisées par le processeur : il calculait l'image à afficher, et l’envoyait à une carte d'affichage 2D. Au fil du temps, de nombreux circuits furent ajoutés, afin de déporter un maximum de calculs vers la carte vidéo. Le rendu 3D moderne est basé sur le placage de texture inverse, avec des coordonnées de texture, une correction de perspective, etc. Mais les anciennes consoles et bornes d'arcade utilisaient le placage de texture direct. Et cela a impacté le hardware des consoles/PCs de l'époque. Avec le placage de texture direct, il était primordial de calculer la géométrie, mais la rasterisation était le fait de VDC améliorés. Aussi, les premières bornes d'arcade 3D et les consoles de 5ème génération disposaient processeurs pour calculer la géométrie et de circuits d'application de textures très particuliers. A l'inverse, les PC utilisaient un rendu inverse, totalement différent. Sur les PC, les premières cartes graphiques avaient un circuit de rastérisation et des unités de textures, mais pas de circuits géométriques. ==Les premières cartes graphiques== Dès les années 70-80, le rendu 3D était utilisé par de nombreuses entreprises industrielles : des applications de visualisation 3D étaient utilisées en architecture, des applications de conception assistée par ordinateur étaient déjà d'utilisation courante, sans compter les simulateurs de vol utilisés par l'armée et les instructeurs qui formaient les pilotes d'avion. Le rendu 3D était aussi étudié au niveau académique, la recherche en 3D était déjà florissante. Il existait même du matériel spécifiquement conçu pour le rendu graphique, mais celui-ci était spécifiquement dédié à des super-calculateurs ou des ''workstations'' (des sortes d'ancêtres des PC, très puissants pour l'époque, mais conçus uniquement pour les entreprises). ===Le début des années 80 : le rendu en fils de fer=== Le tout premier système de ce genre était le '''''Line Drawing System-1''''' de l'entreprise Evans & Sutherland, daté de 1969. Ce n'est ni plus ni moins que le toute premier circuit graphique séparé du processeur ayant existé. C'est en un sens la toute première carte graphique, le tout premier GPU. Il prenait la forme d'un périphérique qui se connectait à l'ordinateur d'un côté et était relié à l'écran de l'autre. Il était compatible avec un grand nombre d'ordinateurs et de processeurs existants. Il a été suivi par plusieurs successeurs, nommés ''Picture System 1, 2'' et le ''PS300 series''. [[File:Evans & Sutherland LDS-1 (1).jpg|vignette|Evans & Sutherland LDS-1 (1)]] Ils permettaient de faire du rendu en fil de fer, sans texture ni même sans polygones colorés. Un tel rendu était utile pour des applications assez limitées : architecture, dessin de molécules pour les entreprises pharmaceutique et certains centres de recherche, l'aérospatiale, etc. Ces cartes graphiques étaient utilisées de concert avec des écrans appelés '''écrans vectoriels''' (''vector display''). Pour simplifier, ils ressemblaient à des écrans CRT, sauf que le faisceau d'électron ne balayait pas l'écran ligne par ligne, mais traçait des lignes arbitraires à l'écran. On lui précisait deux points de coordonnées x1,y1 ; et x2,y2 ; puis l'écran tracait une ligne entre ces deux points. En général, la ligne tracée était maintenue pendant un long moment, entre plusieurs secondes et plusieurs minutes. L'intérieur du circuit était assez simple : un circuit de multiplication de matrice pour les calculs géométriques, un rastériser simplifié (le ''clipping diviser''), un circuit de tracé de lignes, et un processeur de contrôle pour commander les autres circuits. Le fait que ces trois circuits soient séparés permettait une implémentation en pipeline, où plusieurs portions de l'image pouvaient être calculées en même temps : pendant que l'une est dans l'unité géométrique, l'autre est dans le rastériseur et une troisième est en cours de tracé. [[File:Lds1blockdiagram05.svg|centre|vignette|upright=2|Architecture du LDS-1. Le processeur de contrôle n'est pas représenté.]] Le processeur de contrôle exécute un programme qui se charge de commander l'unité géométrique et les autres circuits. Le programme en question est fourni par le programmeur, le LDS-1 est donc totalement programmable. Il lit directement les données nécessaires pour le rendu dans la mémoire de l’ordinateur et le programme exécuté est lui aussi en mémoire principale. Il n'a pas de mémoire vidéo dédiée, il utilise la RAM de l'ordinateur principal. Le multiplieur de matrices est plus complexe qu'on pourrait s'y attendre. Il ne s'agit pas que d'un circuit arithmétique tout simple, mais d'un véritable processeur avec des registres et des instructions machine complexes. Il contient plusieurs registres, l'ensemble mémorisant 4 matrices de 16 nombres chacune (4 lignes de 4 colonnes). Un nombre est codé sur 18 bits. Les registres sont reliés à un ensemble de circuits arithmétiques, des additionneurs et des multiplieurs. Le circuit supporte des instructions de copie entre registres, pour copier une ligne d'une matrice à une autre, des instructions LOAD/STORE pour lire ou écrire dans la mémoire RAM, etc. Il supporte aussi des multiplications en 2D et 3D. Le ''clipping divider'' est un circuit assez complexe, contenant un processeur à accumulateur, une mémoire ROM pour le programme du processeur. Le programme exécuté par le processeur est un petit programme de 62 instructions, stocké dans la ROM. L'algorithme du ''clipping divider'' est décrite dans le papier de recherche "A clipping divider", écrit par Robert Sproull. Un détail assez intéressant est que le résultat en sortie de l'unité géométrique et du rastériseur peuvent être envoyés à l'ordinateur en parallèle du rendu. C'était très utile sur les anciens ordinateurs qui étaient connectés à plusieurs terminaux. Le LDS-1 calculait la géométrie et le rendu, et le tout pouvait petre envoyé à d'autres composants, comme des terminaux, une imprimante, etc. ===Les systèmes ultérieurs : rendu à triangles colorés et texturé=== Les systèmes précédents étaient très limités : ils calculaient la géométrie et n'avaient pas de ''framebuffer'', ni de tampon de profondeur, ni gestion de l'éclairage, ni quoique ce soit. De tels systèmes étaient donc des accélérateurs géométriques que de vrais systèmes graphiques complets, du fait de l'absence de ''framebuffer''. Ils étaient composés de processeurs spécialisés dans les calculs à virgule flottante, faisant des calculs géométriques, et éventuellement d'un processeur pour la rastérisation. La raison est que la RAM était très chère et que créer des circuits fixes étaient très chers et peu disponibles. Par contre, les processeurs à virgule flottante étaient peu chers et facile à trouver. Vers la fin des années 80, grâce à la baisse du prix de la RAM et la démocratisation des ASIC (des circuits fixes fait sur mesure), ajouter un ''framebuffer'' est est devenu possible. C'est alors que sont apparus les '''systèmes de rendu 3D de première génération'''. De tels systèmes ont permis d'implémenter le rendu à primitives colorées qu'on a vu il y a quelques chapitres, à savoir un rendu où les triangles sont coloriés avec une couleur unique. Les systèmes de première génération étaient simples : des processeurs pour le calcul de la géométrie, un circuit de rastérisation, une RAM pour le ''framebuffer'' et des ASIC servant de ROPs très simples. Il n'y avait pas d'élimination des pixels cachés, pas de textures, et encore moins d'éclairage par pixels. Le premier système de ce genre était le ''Shaded Picture System'', toujours par Evans & Sutherland. Il ne gérait pas la couleur et ne pouvait afficher que des images en noir et blanc, mais il gérait l'éclairage par sommet (''vertex lighting''). Il a rapidement été dépassé par les systèmes de l'entreprise ''Silicon Graphics Inc'' (SGI), ainsi que ceux de l'entreprise Apollo avec sa série Apollo DN. Les '''systèmes de seconde génération''' sont apparus vers la fin des années 80, et se distinguent des précédents par l'ajout un tampon de profondeur. Ils intègrent aussi des capacités d'éclairage par pixel, à savoir de l'éclairage plat, de Gouraud, voire de Phong ! Enfin, les '''systèmes de troisième génération''' ont acquis des capacités de placage de texture, que les systèmes précédents n'avaient pas. Ils ont aussi ajouté un support de l'antialiasing. Les systèmes SGI avec placage de texture ont déjà été abordé au chapitre précédent, dans la section sur les GPU en mode immédiat et à ''tile''. Aussi, nous ne reviendrons pas dessus. [[File:Evolution de l'architecture des premières cartes graphiques, dans les années 80-90.png|centre|vignette|upright=2.5|Evolution de l'architecture des premières cartes graphiques, dans les années 80-90]] Les systèmes de première, seconde et troisième génération avaient de nombreux points communs. En premier lieu, ils étaient fabriqués en connectant plusieurs cartes électroniques : une carte pour les calculs géométriques, une ou plusieurs cartes pour le reste du rendu graphique, une carte dédiée au VDC et avec un connecteur écran. Les transistors de l'époque n'étaient pas encore miniaturisés, ce qui fait que le système graphique ne pouvait pas tenir sur une seule carte électronique. Il n'y avait donc pas de carte graphique proprement dit, mais un équivalent éclaté sur plusieurs cartes électroniques. La carte pour la géométrie contenait typiquement une mémoire FIFO pour accumuler les commandes de rendu, un processeur de commande, et plusieurs processeurs géométriques. Les processeurs géométriques étaient parfois conçus sur mesure, comme l'a été le le ''Geometry Engine'' de SGI. Mais il est arrivé qu'ils utilisent des processeurs commerciaux comme le Weitek 3222, l'Intel i860, etc. Les processeurs pouvaient être placés en série ou en parallèle, comme expliqué dans le chapitre précédent. Le circuit de rastérisation était réalisé soit avec un processeur dédié, soit avec un circuit fixe, soit un mélange des deux. La rastérisation est en effet réalisée en plusieurs étapes, certaines peuvent être implémentées avec un processeur et d'autres avec des circuits fixes. Un point important est qu'à l'époque, le rendu n'utilisait pas que des triangles, mais des polygones en général. Ce n'est que par la suite que le rendu s'est focalisé sur les triangles et les ''quads'' (quadrilatères). Il arrivait que le système graphique gérait partiellement des polygones concaves, voire convexes. Sur les systèmes SGI, les calculs géométriques se faisaient avec des polygones, que la rastérisation découpait en triangles, le reste du rendu se faisait avec des triangles. Les stations de travail Apollo DN 10000VS découpaient les polygones en trapézoïdes orientés à l'horizontale, alignés avec des ''scanlines''. D'autres systèmes découpaient tout en triangle lors de l'étape géométrique ==Les précurseurs grand public : les bornes d'arcade== [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] L'accélération du rendu 3D sur les bornes d'arcade était déjà bien avancé dès les années 90. Les bornes d'arcade ont toujours été un segment haut de gamme de l'industrie du jeu vidéo, aussi ce n'est pas étonnant. Le prix d'une borne d'arcade dépassait facilement les 10 000 dollars pour les plus chères et une bonne partie du prix était celui du matériel informatique. Le matériel était donc très puissant et débordait de mémoire RAM comparé aux consoles de jeu et aux PC. La plupart des bornes d'arcade utilisaient du matériel standardisé entre plusieurs bornes. A l'intérieur d'une borne d'arcade se trouve une '''carte de borne d'arcade''' qui est une carte mère avec un ou plusieurs processeurs, de la RAM, une carte graphique, un VDC et pas mal d'autres matériels. La carte est reliée aux périphériques de la borne : joysticks, écran, pédales, le dispositif pour insérer les pièces afin de payer, le système sonore, etc. Le jeu utilisé pour la borne est placé dans une cartouche qui est insérée dans un connecteur spécialisé. Les cartes de bornes d'arcade étaient généralement assez complexes, elles avaient une grande taille et avaient plus de composants que les cartes mères de PC. Chaque carte contenait un grand nombre de chips pour la mémoire RAM et ROM, et il n'était pas rare d'avoir plusieurs processeurs sur une même carte. Et il n'était pas rare d'avoir trois à quatre cartes superposées dans une seule borne. Pour ceux qui veulent en savoir plus, Fabien Sanglard a publié gratuitement un livre sur le fonctionnement des cartes d'arcade CPS System, disponible via ce lien : [https://fabiensanglard.net/b/cpsb.pdf The book of CP System]. Les premières cartes graphiques des bornes d'arcade étaient des cartes graphiques 2D auxquelles on avait ajouté quelques fonctionnalités. Les sprites pouvaient être tournés, agrandit/réduits, ou déformés pour simuler de la perspective et faire de la fausse 3D. Par la suite, le vrai rendu 3D est apparu sur les bornes d'arcade. Dès 1988, la carte d'arcade Namco System 21 et Sega Model 1 géraient les calculs géométriques. Quelques années plus tard, les cartes graphiques se sont mises à supporter un éclairage de Gouraud et du placage de texture. Par exemple, le Namco System 22 et la Sega model 2 supportaient des textures 2D et comme le filtrage de texture (bilinéaire et trilinéaire), le mip-mapping, et quelques autres. Au passage, les cartes graphiques de la Namco System 22 étaient développées en partenariat avec Eans & Sutherland, qui avait commencé à se diversifier dans le marché grand public. Les cartes graphiques de l'époque faisaient les calculs géométriques sur plusieurs processeurs, généralement des processeurs de type DSP (des processeurs spécialisés dans le traitement de signal). Par exemple, la Namco System 2 utilisait 4 DSP de marque Texas Instruments TMS320C25, cadencés à 24,576 MHz. La carte d'arcade Sega Model 1 utilisait quant à elle un DSP spécialisé dans les calculs géométriques. Par la suite, les bornes d'arcade ont réutilisé le hardware des PC et autres consoles de jeux. ==La 3D sur les consoles de quatrième/cinquième génération== Les consoles avant la quatrième génération de console étaient des consoles purement 2D, sans circuits d'accélération 3D. Leur carte graphique était un simple VDC 2D, plus ou moins performant selon la console. Les premières consoles de jeu capables de rendu 3D par elles-mêmes sont les consoles dites de 5ème génération. Il y a diverses manières de classer les consoles en générations, la plus commune place la 3D à la 5ème génération, mais détailler ces controverses quant à ce classement nous amènerait trop loin. Les consoles de génération avaient une architecture assez différente des systèmes antérieurs. Les systèmes SGI et assimilés pouvaient se permettre de couter assez cher, d'utiliser beaucoup de circuits, de prendre beaucoup de place. Les bornes d'arcade sont aussi dans ce cas. Aussi, il n'était pas rare que les cartes 3D de l'époque tiennent sur plusieurs cartes électroniques séparées. Mais une console ne peut pas se permettre ce genre de folies. Aussi, les cartes 3D des consoles de l'époque tenaient dans un seul circuit intégré, comme il est d'usage de nos jours. La conséquence est que certains circuits étaient fortement simplifiés, sur les consoles de cinquième génération. Et cela a impacté l'architecture interne des GPU des consoles. Les systèmes SGI avaient plusieurs processeurs pour calculer la géométrie, couplés à plusieurs unités non-programmables pour les pixels/textures. Les cartes 3D des consoles gardaient cette organisation : processeurs pour la géométrie, circuits fixes pour le reste. Mais elles se débrouillaient souvent avec un seul processeur, voire aucun ! Dans ce dernier cas, la géométrie était calculée sur le processeur principal, le CPU. Les unités pour les pixels étaient aussi moins nombreuses, mais il y en avait plusieurs, pour profiter de l'amplification des pixels. : Les cartes 3D des consoles de jeu utilisaient le placage de texture inverse, avec quelques exceptions qui utilisaient le placage de texture direct. ===Le rendu 3D sur les consoles de quatrième génération : la SNES=== Plus haut, j'ai dit que les consoles de quatrième génération n'avaient pas de carte accélératrice 3D. Pourtant, elles ont connus quelques jeux en vraie 3D. La raison à cela est que la 3D était calculée par un GPU placé dans les cartouches du jeu ! Par exemple, les cartouches de Starfox et de Super Mario 2 contenaient un coprocesseur Super FX, qui gérait des calculs de rendu 2D/3D. En tout, il y a environ 16 coprocesseurs pour la SNES et on en trouve facilement la liste sur le net. La console était conçue pour, des pins sur les ports cartouches étaient prévues pour des fonctionnalités de cartouche annexes, dont ces coprocesseurs. Ces pins connectaient le coprocesseur au bus des entrées-sorties. Les coprocesseurs des cartouches de NES avaient souvent de la mémoire rien que pour eux, qui était intégrée dans la cartouche. Ceci étant dit, passons aux consoles de cinquième génération. ===La Nintendo 64 : un GPU avancé=== La Nintendo 64 avait le GPU le plus complexe comparé aux autres consoles, et dépassait même les cartes graphiques des PC. Il faut dire que son GPU a été conçu avec l'aide de l'entreprise SGI, dont on a vu les systèmes graphiques plus haut. Le GPU de la N64 incorporait une unité pour les calculs géométriques, un circuit de rasterisation, une unité de textures et un ROP final pour les calculs de transparence/brouillard/antialiasing, ainsi qu'un circuit pour gérer la profondeur des pixels. En somme, tout le pipeline graphique était implémenté dans le GPU de la Nintendo 64, chose très en avance sur son temps, comparé au PC ou aux autres consoles ! Le GPU est construit autour d'un processeur dédié aux calculs géométriques, le ''Reality Signal Processor'' (RSP), autour duquel on a ajouté des circuits pour le reste du pipeline graphique. L'unité de calcul géométrique est un processeur MIPS R4000, un processeur assez courant à l'époque, auquel on avait retiré quelques fonctionnalités inutiles pour le rendu 3D. Il était couplé à 4 KB de mémoire vidéo, ainsi qu'à 4 KB de mémoire ROM. Le reste du GPU était réalisé avec des circuits fixes. Un point intéressant est que le programme exécuté par le RSP pouvait être programmé ! Le RSP gérait déjà des espèces de proto-shaders, qui étaient appelés des ''[https://ultra64.ca/files/documentation/online-manuals/functions_reference_manual_2.0i/ucode/microcode.html micro-codes]'' dans la documentation de l'époque. La ROM associée au RSP mémorise cinq à sept programmes différents, aux fonctionnalités différentes. * Les microcodes gspFast3D et gspF3DNoN, implémentent un rendu 3D normal, avec des options de ''clipping'' différentes entre les deux. * Le microcode gspTurbo3D fait la même chose, mais avec moins de fonctionnalités et avec une précision réduite. Il ne gère pas le ''clipping'', l'éclairage par pixel, la correction de perspective, l'antialiasing et quelques autres fonctionnalités. Il gère cependant l'éclairage de Gouraud. Il utilise une ''display list'' simplifiée comparé aux deux microcodes précédents. * Le microcode gspZ-Sort effectue une pré-passe z, à savoir qu'il calcule le tampon de profondeur final de la scène 3D, sans rendre l'image. Cela sert à faire une élimination des pixels cachés parfaite, en logiciel. On calcule le tampon de profondeur pour déterminer quels pixels sont visibles, puis une seconde passe rend l'image en, rejetant les pixels non-visibles. * Le microcode gspSprite2D implémente un rendu 2D émulé : les sprites et arrière-plan sont des rectangles texturés. Le microcode gspS2DEX fait la même chose, mais sert à émuler le rendu de la SNES plus qu'autre chose. * Le microcode gspLine3D ne gére que des lignes, pas de triangles. Il sert pour du rendu en fil de fer. Ils géraient le rendu 3D de manière différente et avec une gestion des ressources différentes. Très peu de studios de jeu vidéo ont développé leur propre microcodes N64, car la documentation était mal faite, que Nintendo ne fournissait pas de support officiel pour cela, que les outils de développement ne permettaient pas de faire cela proprement et efficacement. ===La Playstation 1=== Sur la Playstation 1 le calcul de la géométrie était réalisé par le processeur, la carte graphique gérait tout le reste. Et la carte graphique était un circuit fixe spécialisé dans la rasterisation et le placage de textures. Elle utilisait, comme la Nintendo 64, le placage de texture inverse, qui est apparu ensuite sur les cartes graphiques. ===La 3DO et la Sega Saturn=== La Sega Saturn et la 3DO étaient les deux seules consoles à utiliser le rendu direct. La géométrie était calculée sur le processeur, même si les consoles utilisaient parfois un CPU dédié au calcul de la géométrie. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. La Sega Saturn incorpore trois processeurs et deux GPU. Les deux GPUs sont nommés le VDP1 et le VDP2. Le VDP1 s'occupe des textures et des sprites, le VDP2 s'occupe uniquement de l'arrière-plan et incorpore un VDC tout ce qu'il y a de plus simple. Ils ne gèrent pas du tout la géométrie, qui est calculée par les trois processeurs. Le troisième processeur, la Saturn Control Unit, est un processeur de type DSP, à savoir un processeur spécialisé dans le traitement de signal. Il est utilisé presque exclusivement pour accélérer les calculs géométriques. Il avait sa propre mémoire RAM dédiée, 32 KB de SRAM, soit une mémoire locale très rapide. Les transferts entre cette RAM et le reste de l'ordinateur était géré par un contrôleur DMA intégré dans le DSP. En somme, il s'agit d'une sorte de processeur spécialisé dans la géométrie, une sorte d'unité géométrique programmable. Mais la géométrie n'était pas forcément calculée que sur ce DSP, mais pouvait être prise en charge par les 3 CPU. ==L'historique des cartes graphiques des PC, avant l'arrivée de Direct X et Open Gl== Sur PC, l'évolution des cartes graphiques a eu du retard par rapport aux consoles. Les PC sont en effet des machines multi-usage, pour lesquelles le jeu vidéo était un cas d'utilisation parmi tant d'autres. Et les consoles étaient la plateforme principale pour jouer à des jeux vidéo, le jeu vidéo PC étant plus marginal. Mais cela ne veut pas dire que le jeu PC n'existait pas, loin de là ! Un problème pour les jeux PC était que l'écosystème des PC était aussi fragmenté en plusieurs machines différentes : machines Apple 1 et 2, ordinateurs Commdore et Amiga, IBM PC et dérivés, etc. Aussi, programmer des jeux PC n'était pas mince affaire, car les problèmes de compatibilité étaient légion. C'est seulement quand la plateforme x86 des IBM PC s'est démocratisée que l'informatique grand public s'est standardisée, réduisant fortement les problèmes de compatibilité. Mais cela n'a pas suffit, il a aussi fallu que les API 3D naissent. Les API 3D comme Direct X et Open GL sont absolument cruciales pour garantir la compatibilité entre plusieurs ordinateurs aux cartes graphiques différentes. Aussi, l'évolution des cartes graphiques pour PC s'est faite main dans la main avec l'évolution des API 3D. Les fonctionnalités des cartes graphiques ont évolué dans le temps, en suivant les évolutions des API 3D. Du moins dans les grandes lignes, car il est arrivé plusieurs fois que des fonctionnalités naissent sur les cartes graphiques, pour que les fabricants forcent la main de Microsoft ou d'Open GL pour les intégrer de force dans les API 3D. Passons. ===L'introduction des premiers jeux 3D : Quake et les drivers miniGL=== L'API OpenGL est née de la main de SGI, encore eux ! SGI avait créé l'API Iris GL pour ses stations de travail Iris Graphics. Iris GL a ensuite été libéré et est devenu le standard Open GL. Open GL existait déjà avant l'apparition des cartes accélératrices 3D. Il y a avait donc déjà un terreau que les programmeurs graphiques pouvaient utiliser. Mais Open GL était surtout utilisé pour des applications industrielles, médicales (imagerie), graphiques ou militaires, pas pour le jeu vidéo. Mais cela changea avec la sortie du jeu Quake, d'IdSoftware, en 1996. Quake pouvait fonctionner en rendu logiciel, mais le programmeur responsable du moteur 3D (le célébre John Carmack) ajouta une version OpenGL du jeu. Il faut dire que le jeu était programmé sur une station de travail compatible avec OpenGL, même si aucune carte accélératrice de l'époque ne supportait OpenGL. C'était là un choix qui se révéla visionnaire. En théorie, le rendu par OpenGL aurait dû se faire intégralement en logiciel, sauf sur quelques rares stations de travail adaptées. Mais les premières cartes graphiques étaient déjà dans les starting blocks. La toute première carte 3D pour PC est la '''Rendition Vérité V1000''', sortie en Septembre 1995, soit quelques mois avant l'arrivée de la Nintendo 64. La Rendition Vérité V1000 contenait un processeur MIPS cadencé à 25 MHz, 4 mébioctets de RAM, une ROM pour le BIOS, et un RAMDAC, rien de plus. C'était un vrai ordinateur complètement programmable de bout en bout, sans aucun circuit fixe. Les programmeurs ne pouvaient cependant pas utiliser cette programmabilité avec des ''shaders'', mais elle permettait à Rendition d'implémenter n'importe quelle API 3D, que ce soit OpenGL, DirectX ou même sa son API propriétaire. La Rendition Vérité avait de bonnes performances pour ce qui est de la géométrie, mais pas pour le reste. Réaliser la rastérisation et le placage de texture en logiciel n'est pas efficace, pareil pour les opérations de fin de pipeline comme l'antialiasing. Le manque d'unités fixes très rapides pour la rastérisation, le placage de texture ou les opérations de fin de pipeline était clairement un gros défaut. Mais la Rendition Vérité était un cas à part, une exception dans le paysage des cartes 3D de l'époque, qui ne faisait rien comme les autres. Les autres cartes graphiques, sorties peu après, étaient les Voodoo de 3dfx, les Riva TNT de NVIDIA, les Rage/3D d'ATI, la Virge/3D de S3, et la Matrox Mystique. Elles avaient choisit le compromis inverse de la Rendition Vérité V1000 : de bonnes performances pour le placage de textures et la rastérization, mais pas pour les calculs géométriques. Pour rappel, les systèmes professionnels et les consoles avaient des processeurs pour la géométrie, et des circuits fixes pour le reste. Les cartes graphiques de PC se passaient des processeurs pour la géométrie, les calculs géométriques étaient réalisés par le CPU. Les toutes premières cartes 3D pour PC contenaient seulement des circuits pour gérer les textures et des ROPs. Elle géraient le ''z-buffer'' en mémoire vidéo, ainsi que des effets de brouillard. Il n'y avait même pas de circuit pour la rastérisation, qui était faite en logiciel, avec les calculs géométriques. [[File:Architecture de base d'une carte 3D - 2.png|centre|vignette|upright=1.5|Carte 3D sans rasterization matérielle.]] Les cartes suivantes ajoutèrent une gestion des étapes de ''rasterization'' directement en matériel. Les cartes ATI rage 2, les Invention de chez Rendition, et d'autres cartes graphiques supportaient la rasterisation en hardware. [[File:Architecture de base d'une carte 3D - 3.png|centre|vignette|upright=1.5|Carte 3D avec gestion de la géométrie.]] Pour exploiter les unités de texture et le circuit de rastérisation, OpenGL et Direct 3D étaient partiellement implémentées en logiciel, car les cartes graphiques ne supportaient pas toutes les fonctionnalités de l'API. C'était l'époque du miniGL, des implémentations partielles d'OpenGL, fournies par les fabricants de cartes 3D, implémentées dans les pilotes de périphériques de ces dernières. Les fonctionnalités d'OpenGL implémentées dans ces pilotes étaient presque toutes exécutées en matériel, par la carte graphique. Avec l'évolution du matériel, les pilotes de périphériques devinrent de plus en plus complets, au point de devenir des implémentations totales d'OpenGL. Mais au-delà d'OpenGL, chaque fabricant de carte graphique avait sa propre API propriétaire, qui était gérée par leurs pilotes de périphériques (''drivers''). Par exemple, les premières cartes graphiques de 3dfx interactive, les fameuses voodoo, disposaient de leur propre API graphique, l'API Glide. Elle facilitait la gestion de la géométrie et des textures, ce qui collait bien avec l'architecture de ces cartes 3D. Mais ces API propriétaires tombèrent rapidement en désuétude avec l'évolution de DirectX et d'OpenGL. Direct X était une API dans l'ombre d'Open GL. La première version de Direct X qui supportait la 3D était DirectX 2.0 (juin 2, 1996), suivie rapidement par DirectX 3.0 (septembre 1996). Elles dataient d'avant le jeu Quake, et elles étaient très éloignées du hardware des premières cartes graphiques. Elles utilisaient un système d'''execute buffer'' pour communiquer avec la carte graphique, Microsoft espérait que le matériel 3D implémenterait ce genre de système. Ce qui ne fu pas le cas. Direct X 4.0 a été abandonné en cours de développement pour laisser à une version 5.0 assez semblable à la 2.0/3.0. Le mode de rendu laissait de côté les ''execute buffer'' pour coller un peu plus au hardware de l'époque. Mais rien de vraiment probant comparé à Open GL. Même Windows utilisait Open GL au lieu de Direct X maison... C'est avec Direct X 6.0 que Direct X est entré dans la cours des grands. Il gérait la plupart des technologies supportées par les cartes graphiques de l'époque. ===Le ''multi-texturing'' de l'époque Direct X 6.0 : combiner plusieurs textures=== Une technologie très importante standardisée par Dirext X 6 est la technique du '''''multi-texturing'''''. Avec ce qu'on a dit dans le chapitre précédent, vous pensez sans doute qu'il n'y a qu'une seule texture par objet, qui est plaquée sur sa surface. Mais divers effet graphiques demandent d'ajouter des textures par dessus d'autres textures. En général, elles servent pour ajouter des détails, du relief, sur une surface pré-existante. Un exemple intéressant vient des jeux de tir : ajouter des impacts de balles sur les murs. Pour cela, on plaque une texture d'impact de balle sur le mur, à la position du tir. Il s'agit là d'un exemple de '''''decals''''', des petites textures ajoutées sur les murs ou le sol, afin de simuler de la poussière, des impacts de balle, des craquelures, des fissures, des trous, etc. Les textures en question sont de petite taille et se superposent à une texture existante, plus grande. Rendre des ''decals'' demande de pouvoir superposer deux textures. Direct X 6.0 supportait l'application de plusieurs textures directement dans le matériel. La carte graphique devait être capable d'accéder à deux textures en même temps, ou du moins faire semblant que. Pour cela, elle doublaient les unités de texture et adaptaient les connexions entre unités de texture et mémoire vidéo. La mémoire vidéo devait être capable de gérer plusieurs accès mémoire en même temps et devait alors avoir un débit binaire élevé. [[File:Multitexturing.png|centre|vignette|upright=2|Multitexturing]] La carte graphique devait aussi gérer de quoi combiner deux textures entre elles. Par exemple, pour revenir sur l'exemple d'une texture d'impact de balle, il faut que la texture d'impact recouvre totalement la texture du mur. Dans ce cas, la combinaison est simple : la première texture remplace l'ancienne, là où elle est appliquée. Mais les cartes graphiques ont ajouté d'autres combinaisons possibles, par exemple additionner les deux textures entre elle, faire une moyenne des texels, etc. Les opérations pour combiner les textures était le fait de circuits appelés des '''''combiners'''''. Concrètement, les ''combiners'' sont de simples unités de calcul. Les ''conbiners'' ont beaucoup évolués dans le temps, mais les premières implémentation se limitaient à quelques opérations simples : addition, multiplication, superposition, interpolation. L'opération effectuer était envoyée au ''conbiner'' sur une entrée dédiée. [[File:Multitexturing avec combiners.png|centre|vignette|upright=2|Multitexturing avec combiners]] S'il y avait eu un seul ''conbiner'', le circuit de ''multitexturing'' aurait été simplement configurable. Mais dans la réalité, les premières cartes utilisant du ''multi-texturing'' utilisaient plusieurs ''combiners'' placés les uns à la suite des autres. L'implémentation des ''combiners'' retenue par Open Gl, et par le hardware des cartes graphiques, était la suivante. Les ''combiners'' étaient placés en série, l'un à la suite de l'autre, chacun combinant le résultat de l'étage précédent avec une texture. Le premier ''combiner'' gérait l'éclairage par sommet, afin de conserver un minimum de rétrocompatibilité. [[File:Texture combiners Open GL.png|centre|vignette|upright=2|Texture combiners Open GL]] Voici les opérations supportées par les ''combiners'' d'Open GL. Ils prennent en entrée le résultat de l'étage précédent et le combinent avec une texture lue depuis l'unité de texture. {|class="wikitable" |+ Opérations supportées par les ''combiners'' d'Open GL |- ! Replace | colspan="2" | Pixel provenant de l'unité de texture |- ! Addition | colspan="2" | Additionne l'entrée au texel lu. |- ! Modulate | colspan="2" | Multiplie l'entrée avec le texel lu |- ! Mélange (''blending'') | Moyenne pondérée des deux entrées, pondérée par la composante de transparence || La couleur de transparence du texel lu et de l'entrée sont multipliées. |- ! Decals | Moyenne pondérée des deux entrées, pondérée par la composante de transparence. || La transparence du résultat est celle de l'entrée. |} Il faut noter qu'un dernier étage de ''combiners'' s'occupait d'ajouter la couleur spéculaire et les effets de brouillards. Il était à part des autres et n'était pas configurable, c'était un étage fixe, qui était toujours présent, peu importe le nombre de textures utilisé. Il était parfois appelé le '''''combiner'' final''', terme que nous réutiliserons par la suite. Mine de rien, cela a rendu les cartes graphiques partiellement programmables. Le fait qu'il y ait des opérations enchainées à la suite, opérations qu'on peut choisir librement, suffit à créer une sorte de mini-programme qui décide comment mélanger plusieurs textures. Mais il y avait une limitation de taille : le fait que les données soient transmises d'un étage à l'autre, sans détours possibles. Par exemple, le troisième étage ne pouvait avoir comme seule opérande le résultat du second étage, mais ne pouvait pas utiliser celui du premier étage. Il n'y avait pas de registres pour stocker ce qui sortait de la rastérisation, ni pour mémoriser temporairement les texels lus. ===Le ''Transform & Lighting'' matériel de Direct X 7.0=== [[File:Architecture de base d'une carte 3D - 4.png|vignette|upright=1.5|Carte 3D avec gestion de la géométrie.]] La première carte graphique pour PC capable de gérer la géométrie en hardware fût la Geforce 256, la toute première Geforce. Son unité de gestion de la géométrie n'est autre que la bien connue '''unité T&L''' (''Transform And Lighting''). Elle implémentait des algorithmes d'éclairage de la scène 3D assez simples, comme un éclairage de Gouraud, qui étaient directement câblés dans ses circuits. Mais contrairement à la Nintendo 64 et aux bornes d'arcade, elle implémentait le tout, non pas avec un processeur classique, mais avec des circuits fixes. Avec Direct X 7.0 et Open GL 1.0, l'éclairage était en théorie limité à de l'éclairage par sommet, l'éclairage par pixel n'était pas implémentable en hardware. Les cartes graphiques ont tenté d'implémenter l'éclairage par pixel, mais cela n'est pas allé au-delà du support de quelques techniques de ''bump-mapping'' très limitées. Par exemple, Direct X 6.0 implémentait une forme limitée de ''bump-mapping'', guère plus. Un autre problème est qu'il a beaucoup d'algorithmes d'éclairages différents, aux résultats visuels différents, bien au-delà des algorithmes d'éclairage plat, de Gouraud et de Phong. Et les unités de T&L étaient souvent en retard sur les algorithmes logiciels. Les programmeurs avaient le choix entre programmer les algorithmes d’éclairage qu'ils voulaient et les exécuter en logiciel, ou utiliser ceux de l'unité de T&L. Ils choisissaient souvent la première option. Par exemple, Quake 3 Arena et Unreal Tournament n'utilisaient pas les capacités d'éclairage géométrique et préféraient utiliser leurs calculs d'éclairage logiciel fait maison. Cependant, le hardware dépassait les capacités des API et avait déjà commencé à ajouter des capacités de programmation liées au ''multi-texturing''. Les cartes graphiques de l'époque, surtout chez NVIDIA, implémentaient un système de '''''register combiners''''', une forme améliorée de ''texture combiners'', qui permettait de faire une forme limitée d'éclairage par pixel, notamment du vrai ''bump-mampping'', voire du ''normal-mapping''. Mais ce n'était pas totalement supporté par les API 3D de l'époque. Les ''registers combiners'' sont des ''texture combiners'' mais dans lesquels ont aurait retiré la stricte organisation en série. Il y a toujours plusieurs étages à la suite, qui peuvent exécuter chacun une opération, mais tous les étages ont maintenant accès à toutes les textures lues et à tout ce qui sort de la rastérisation, pas seulement au résultat de l'étape précédente. Pour cela, on ajoute des registres pour mémoriser ce qui sort des unités de texture, et pour ce qui sort de la rastérisation. De plus, on ajoute des registres temporaires pour mémoriser les résultats de chaque ''combiner'', de chaque étage. Il faut cependant signaler qu'il existe un ''combiner'' final, séparé des étages qui effectuent des opérations proprement dits. Il s'agit de l'étage qui applique la couleur spéculaire et les effets de brouillards. Il ne peut être utilisé qu'à la toute fin du traitement, en tant que dernier étage, on ne peut pas mettre d'opérations après lui. Sa sortie est directement connectée aux ROPs, pas à des registres. Il faut donc faire la distinction entre les '''''combiners'' généraux''' qui effectuent une opération et mémorisent le résultat dans des registres, et le ''combiner'' final qui envoie le résultat aux ROPs. L'implémentation des ''register combiners'' utilisait un processeur spécialisés dans les traitements sur des pixels, une sorte de proto-processeur de ''shader''. Le processeur supportait des opérations assez complexes : multiplication, produit scalaire, additions. Il s'agissait d'un processeur de type VLIW, qui sera décrit dans quelques chapitres. Mais ce processeur avait des programmes très courts. Les premières cartes NVIDIA, comme les cartes TNT pouvaient exécuter deux opérations à la suite, suivie par l'application de la couleurs spéculaire et du brouillard. En somme, elles étaient limitées à un ''shader'' à deux/trois opérations, mais c'était un début. Le nombre d'opérations consécutives est rapidement passé à 8 sur la Geforce 3. ===L'arrivée des ''shaders'' avec Direct X 8.0=== [[File:Architecture de la Geforce 3.png|vignette|upright=1.5|Architecture de la Geforce 3]] Les ''register combiners'' était un premier pas vers un éclairage programmable. Paradoxalement, l'évolution suivante s'est faite non pas dans l'unité de rastérisation/texture, mais dans l'unité de traitement de la géométrie. La Geforce 3 a remplacé l'unité de T&L par un processeur capable d'exécuter des programmes. Les programmes en question complétaient l'unité de T&L, afin de pouvoir rajouter des techniques d'éclairage plus complexes. Le tout a permis aussi d'ajouter des animations, des effets de fourrures, des ombres par ''shadow volume'', des systèmes de particule évolués, et bien d'autres. À partir de la Geforce 3 de Nvidia, les cartes graphiques sont devenues capables d'exécuter des programmes appelés '''''shaders'''''. Le terme ''shader'' vient de ''shading'' : ombrage en anglais. Grace aux ''shaders'', l'éclairage est devenu programmable, il n'est plus géré par des unités d'éclairage fixes mais été laissé à la créativité des programmeurs. Les programmeurs ne sont plus vraiment limités par les algorithmes d'éclairage implémentés dans les cartes graphiques, mais peuvent implémenter les algorithmes d'éclairage qu'ils veulent et peuvent le faire exécuter directement sur la carte graphique. Les ''shaders'' sont classifiés suivant les données qu'ils manipulent : '''''pixel shader''''' pour ceux qui manipulent des pixels, '''''vertex shaders''''' pour ceux qui manipulent des sommets. Les premiers sont utilisés pour implémenter l'éclairage par pixel, les autres pour gérer tout ce qui a trait à la géométrie, pas seulement l'éclairage par sommets. Direct X 8.0 avait un standard pour les shaders, appelé ''shaders 1.0'', qui correspondait parfaitement à ce dont était capable la Geforce 3. Il standardisait les ''vertex shaders'' de la Geforce 3, mais il a aussi renommé les ''register combiners'' comme étant des ''pixel shaders'' version 1.0. Les ''register combiners'' n'ont pas évolués depuis la Geforce 256, si ce n'est que les programmes sont passés de deux opérations successives à 8, et qu'il y avait possibilité de lire 4 textures en ''multitexturing''. A l'opposé, le processeur de ''vertex shader'' de la Geforce 3 était capable d'exécuter des programmes de 128 opérations consécutives et avait 258 registres différents ! Des ''pixels shaders'' plus évolués sont arrivés avec l'ATI Radeon 8500 et ses dérivés. Elle incorporait la technologie ''SMARTSHADER'' qui remplacait les ''registers combiners'' par un processeur de ''shader'' un peu limité. Un point est que le processeur acceptait de calculer des adresses de texture dans le ''pixel shader''. Avant, les adresses des texels à lire étaient fournis par l'unité de rastérisation et basta. L'avantage est que certains effets graphiques étaient devenus possibles : du ''bump-mapping'' avancé, des textures procédurales, de l'éclairage par pixel anisotrope, du éclairage de Phong réel, etc. Avec la Radeon 8500, le ''pixel shader'' pouvait calculer des adresses, et lire les texels associés à ces adresses calculées. Les ''pixel shaders'' pouvaient lire 6 textures, faire 8 opérations sur les texels lus, puis lire 6 textures avec les adresses calculées à l'étape précédente, et refaire 8 opérations. Quelque chose de limité, donc, mais déjà plus pratique. Les ''pixel shaders'' de ce type ont été standardisé dans Direct X 8.1, sous le nom de ''pixel shaders 1.4''. Encore une fois, le hardware a forcé l'intégration dans une API 3D. ===Les ''shaders'' de Direct X 9.0 : de vrais ''pixel shaders''=== Avec Direct X 9.0, les ''shaders'' sont devenus de vrais programmes, sans les limitations des ''shaders'' précédents. Les ''pixels shaders'' sont passés à la version 2.0, idem pour les ''vertex shaders''. Concrètement, ils ont des fonctionnalités bien supérieures à celles des ''registers combiners''. Les ''shaders'' pouvaient exécuter une suite d'opérations arbitraire, dans le sens où elle n'était pas structurée avec tel type d'opération au début, suivie par un accès aux textures, etc. On pouvait mettre n'importe quelle opération dans n'importe quel ordre. De plus, les ''shaders'' ne sont plus écrit en assembleur comme c'était le cas avant. Ils sont dorénavant écrits dans un langage de haut-niveau, le HLSL pour les shaders Direct X et le GLSL pour les shaders Open Gl. Les ''shaders'' sont ensuite traduit (compilés) en instructions machines compréhensibles par la carte graphique. Au début, ces langages et la carte graphique supportaient uniquement des opérations simples. Mais au fil du temps, les spécifications de ces langages sont devenues de plus en plus riches à chaque version de Direct X ou d'Open Gl, et le matériel en a fait autant. Le matériel s'est alors adapté, en incorporant un véritable processeur pour les ''pixel shaders''. Les ''pixel shaders'' sont maintenant exécutés par un processeur de ''shader'' dédié, aux fonctionnalités bien supérieures à celles des ''registers combiners''. Le processeur de ''pixel shader'' incorpore l'unité de texture en sont sein, les deux sont fusionnés. La raison à cela sera expliqué dans la suite du chapitre. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=1.5|Carte 3D avec pixels et vertex shaders non-unifiés.]] ===L'après Direct X 9.0=== Avant Direct X 10, les processeurs de ''shaders'' ne géraient pas exactement les mêmes opérations pour les processeurs de ''vertex shader'' et de ''pixel shader''. Les processeurs de ''vertex shader'' et de ''pixel shader''étaient séparés. Depuis DirectX 10, ce n'est plus le cas : le jeu d'instructions a été unifié entre les vertex shaders et les pixels shaders, ce qui fait qu'il n'y a plus de distinction entre processeurs de vertex shaders et de pixels shaders, chaque processeur pouvant traiter indifféremment l'un ou l'autre. [[File:Architecture de base d'une carte 3D - 6.png|centre|vignette|upright=1.5|Architecture de la GeForce 6800.]] Avec Direct X 10, de nombreux autres ''shaders'' sont apparus. Les plus liés au rendu 3D sont les '''''geometry shader''''' pour ceux qui manipulent des triangles, de ''hull shaders'' et de ''domain shaders'' pour la tesselation. De plus, les cartes graphiques modernes sont capables d’exécuter des programmes informatiques qui n'ont aucun lien avec le rendu 3D, mais sont exécutés par la carte graphique comme le ferait un processeur d'ordinateur normal. De tels ''shaders'' sans lien avec le rendu 3D sont appelés des ''compute shader''. ==Les cartes graphiques d'aujourd'hui== Avec l'arrivée des shaders, les circuits d'une carte graphique sont divisés en deux catégories : d'un côté les circuits non-programmables et de l'autre les circuits programmables. Pour exécuter les ''shaders'', la carte graphique incorpore des '''processeurs de ''shaders''''', des processeurs similaires aux processeurs des ordinateurs, aux CPU, mais avec quelques petites différences qu'on expliquera dans le prochain chapitre. A côté des processeurs de ''shaders'', il reste quelques circuits non-programmables appelés des circuits fixes. De nos jours, la gestion de la géométrie et des pixels est programmable, mais la rastérisation, le placage de texture, le ''culling'' et l'enregistrement du ''framebuffer'' ne l'est pas. Il n'en a pas toujours été ainsi. [[File:3D-Pipeline.svg|centre|vignette|upright=3.0|Pipeline 3D : ce qui est programmable et ce qui ne l'est pas dans une carte graphique moderne.]] ===Les GPU modernes sont un mélange de processeurs et de circuits fixes=== Une carte graphique contient donc un mélange de circuits fixes et de processeurs de ''shaders'', qui peut sembler contradictoire. Pourquoi ne pas tout rendre programmable ? Ou au contraire, utiliser seulement des circuits fixes ? La réponse rapide est qu'il s'agit d'un compromis entre flexibilité et performance qui permet d'avoir le meilleur des deux mondes. Mais ce compromis a fortement évolué dans le temps, comme on va le voir plus bas. Rendre la gestion de la géométrie ou des pixels programmable permet d'implémenter facilement un grand nombre d'effets graphiques sans avoir à les implémenter en hardware. Avant les ''shaders'', seul le hardware récent gérait les dernières fonctionnalités. Les effets graphiques derniers cri n'étaient disponibles que sur les derniers modèles de carte graphique. Avec des ''vertex/pixel shaders'', ce genre de défaut est passé à la trappe. Si un nouvel algorithme de rendu graphique est inventé, il peut être utilisé dès le lendemain sur toutes les cartes graphiques modernes. De plus, implémenter beaucoup d'algorithmes d'éclairage différents est difficile. Le cout en termes de transistors et de complexité était assez important, utiliser des circuits programmable a un cout en hardware plus limité. Tout cela est à l'exact opposé de ce qu'on a avec les autres circuits, comme les circuits pour la rastérisation ou le placage de texture. Il n'y a pas 36 façons de rastériser une scène 3D et la flexibilité n'est pas un besoin important pour cette opération, alors que les performances sont cruciales. Même chose pour le placage/filtrage de textures. En conséquences, les unités de transformation, de rastérisation et de placage de texture sont toutes implémentées en matériel. Faire ainsi permet de gagner en performance sans que cela ait le moindre impact pour le programmeur. ===Les unités de texture sont intégrées aux processeurs de shaders=== Avec l'arrivée des processeurs de shaders, les unités de texture ont été intégrées dans les processeurs de shaders eux-mêmes. C'est la seule unité fixe qui a subit ce traitement, et il est intéressant de comprendre pourquoi. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=2|Architecture de base d'une carte 3D.]] Pour cela, il faut faire un rappel sur ce qu'il y a dans un processeur. Un processeur contient globalement quatre circuits : * une unité de calcul qui fait des calculs ; * des registres pour stocker les opérandes et résultats des calculs ; * une unité de communication avec la mémoire ; * et un séquenceur, un circuit de contrôle qui commande les autres. L'unité de communication avec la mémoire sert à lire ou écrire des données, à les transférer de la RAM vers les registres, ou l'inverse. Lire une donnée demande d'envoyer son adresse à la RAM, qui répond en envoyant la donnée lue. Elle est donc toute indiquée pour lire une texture : lire une texture n'est qu'un cas particulier de lecture de données. Les texels à lire sont à une adresse précise, la RAM répond à la lecture avec le texel demandé. Il est donc possible d'utiliser l'unité de communication avec la mémoire comme si c'était une unité de texture. Cependant, les textures ne sont pas utilisées comme telles de nos jours. Le rendu 3D moderne utilise des techniques dites de filtrage de texture, qui permettent d'améliorer la qualité du rendu des textures. Sans ce filtrage de texture, les textures appliquées naïvement donnent un résultat assez pixelisé et assez moche, pour des raisons assez techniques. Le filtrage élimine ces artefacts, en utilisant une forme d'''antialiasing'' interne aux textures, le fameux filtrage de texture. Le filtrage de texture peut être réalisé en logiciel ou en matériel. Techniquement, il est possible de le faire dans un ''shader''. Le ''shader'' calcule les adresses des texels à lire, lit les texels, et effectue ensuite le filtrage avec des opérations de calcul. Mais ce n'est pas ce qui est fait, le filtrage de texture est toujours effectué directement en matériel. La raison est que le filtrage de texture est très simple à implémenter en hardware. Le filtrage bilinéaire ou trilinéaire demande juste des circuits d'interpolation et quelques registres, ce qui est trivial. Et la seconde raison est qu'il n'y a pas 36 façons de filtrer des textures : une carte graphique peut implémenter les algorithmes principaux existants en assez peu de circuits. Pour simplifier l'implémentation, les processeurs de ''shader'' modernes disposent d'une unité d'accès mémoire séparée de l'unité de texture. L'unité d'accès mémoire normale s'occupe des accès mémoire hors-textures, alors que l'unité mémoire s'occupe de lire les textures. L'unité de texture contient de quoi faire du filtrage de texture, mais aussi faire des calculs d'adresse spécialisées, intrinsèquement liés au format des textures, qu'on détaillera dans le chapitre sur les textures. En comparaison, les unités d'accès mémoire effectuent des calculs d'adresse plus basiques. Un dernier avantage est que l'unité de texture est reliée au cache de texture, alors que l'unité d'accès mémoire est relié au cache L1/L2. : Il faut noter que les ROPs peuvent aussi être intégré dans les processeurs de ''shader'', mais c'est assez rare. Les cartes graphiques pour PC ont des ROPs séparés des processeurs de ''shaders''. Par contre, quelques cartes graphiques destinées les smartphones et autres appareils mobiles font exception. Sur celles-ci, les unités de textures et les ROPs sont intégrés dans les processeurs de ''shaders'', aux côtés de l'unité d'accès mémoire LOAD/STORE. Il s'agit de cartes graphiques en rendu à tuiles. {{NavChapitre | book=Les cartes graphiques | prev=Les cartes graphiques : architecture de base | prevText=Les cartes graphiques : architecture de base | next=Les processeurs de shaders | nextText=Les processeurs de shaders }} {{autocat}} 2ebd1w88yga6p6fbryzghhle58wf9ev 763501 763500 2026-04-11T19:22:50Z Mewtow 31375 /* Les GPU modernes sont un mélange de processeurs et de circuits fixes */ 763501 wikitext text/x-wiki Il est intéressant d'étudier le hardware des cartes graphiques en faisant un petit résumé de leur évolution dans le temps. En effet, leur hardware a fortement évolué dans le temps. Et il serait difficile à comprendre le hardware actuel sans parler du hardware d'antan. En effet, une carte graphique moderne est partiellement programmable. Certains circuits sont totalement programmables, d'autres non. Et pour comprendre pourquoi, il faut étudier comment ces circuits ont évolués. Le hardware des cartes graphiques a fortement évolué dans le temps, ce qui n'est pas une surprise. Les évolutions de la technologie, avec la miniaturisation des transistors et l'augmentation de leurs performances a permis aux cartes graphiques d'incorporer de plus en plus de circuits avec les années. Avant l'invention des cartes graphiques, toutes les étapes du pipeline graphique étaient réalisées par le processeur : il calculait l'image à afficher, et l’envoyait à une carte d'affichage 2D. Au fil du temps, de nombreux circuits furent ajoutés, afin de déporter un maximum de calculs vers la carte vidéo. Le rendu 3D moderne est basé sur le placage de texture inverse, avec des coordonnées de texture, une correction de perspective, etc. Mais les anciennes consoles et bornes d'arcade utilisaient le placage de texture direct. Et cela a impacté le hardware des consoles/PCs de l'époque. Avec le placage de texture direct, il était primordial de calculer la géométrie, mais la rasterisation était le fait de VDC améliorés. Aussi, les premières bornes d'arcade 3D et les consoles de 5ème génération disposaient processeurs pour calculer la géométrie et de circuits d'application de textures très particuliers. A l'inverse, les PC utilisaient un rendu inverse, totalement différent. Sur les PC, les premières cartes graphiques avaient un circuit de rastérisation et des unités de textures, mais pas de circuits géométriques. ==Les premières cartes graphiques== Dès les années 70-80, le rendu 3D était utilisé par de nombreuses entreprises industrielles : des applications de visualisation 3D étaient utilisées en architecture, des applications de conception assistée par ordinateur étaient déjà d'utilisation courante, sans compter les simulateurs de vol utilisés par l'armée et les instructeurs qui formaient les pilotes d'avion. Le rendu 3D était aussi étudié au niveau académique, la recherche en 3D était déjà florissante. Il existait même du matériel spécifiquement conçu pour le rendu graphique, mais celui-ci était spécifiquement dédié à des super-calculateurs ou des ''workstations'' (des sortes d'ancêtres des PC, très puissants pour l'époque, mais conçus uniquement pour les entreprises). ===Le début des années 80 : le rendu en fils de fer=== Le tout premier système de ce genre était le '''''Line Drawing System-1''''' de l'entreprise Evans & Sutherland, daté de 1969. Ce n'est ni plus ni moins que le toute premier circuit graphique séparé du processeur ayant existé. C'est en un sens la toute première carte graphique, le tout premier GPU. Il prenait la forme d'un périphérique qui se connectait à l'ordinateur d'un côté et était relié à l'écran de l'autre. Il était compatible avec un grand nombre d'ordinateurs et de processeurs existants. Il a été suivi par plusieurs successeurs, nommés ''Picture System 1, 2'' et le ''PS300 series''. [[File:Evans & Sutherland LDS-1 (1).jpg|vignette|Evans & Sutherland LDS-1 (1)]] Ils permettaient de faire du rendu en fil de fer, sans texture ni même sans polygones colorés. Un tel rendu était utile pour des applications assez limitées : architecture, dessin de molécules pour les entreprises pharmaceutique et certains centres de recherche, l'aérospatiale, etc. Ces cartes graphiques étaient utilisées de concert avec des écrans appelés '''écrans vectoriels''' (''vector display''). Pour simplifier, ils ressemblaient à des écrans CRT, sauf que le faisceau d'électron ne balayait pas l'écran ligne par ligne, mais traçait des lignes arbitraires à l'écran. On lui précisait deux points de coordonnées x1,y1 ; et x2,y2 ; puis l'écran tracait une ligne entre ces deux points. En général, la ligne tracée était maintenue pendant un long moment, entre plusieurs secondes et plusieurs minutes. L'intérieur du circuit était assez simple : un circuit de multiplication de matrice pour les calculs géométriques, un rastériser simplifié (le ''clipping diviser''), un circuit de tracé de lignes, et un processeur de contrôle pour commander les autres circuits. Le fait que ces trois circuits soient séparés permettait une implémentation en pipeline, où plusieurs portions de l'image pouvaient être calculées en même temps : pendant que l'une est dans l'unité géométrique, l'autre est dans le rastériseur et une troisième est en cours de tracé. [[File:Lds1blockdiagram05.svg|centre|vignette|upright=2|Architecture du LDS-1. Le processeur de contrôle n'est pas représenté.]] Le processeur de contrôle exécute un programme qui se charge de commander l'unité géométrique et les autres circuits. Le programme en question est fourni par le programmeur, le LDS-1 est donc totalement programmable. Il lit directement les données nécessaires pour le rendu dans la mémoire de l’ordinateur et le programme exécuté est lui aussi en mémoire principale. Il n'a pas de mémoire vidéo dédiée, il utilise la RAM de l'ordinateur principal. Le multiplieur de matrices est plus complexe qu'on pourrait s'y attendre. Il ne s'agit pas que d'un circuit arithmétique tout simple, mais d'un véritable processeur avec des registres et des instructions machine complexes. Il contient plusieurs registres, l'ensemble mémorisant 4 matrices de 16 nombres chacune (4 lignes de 4 colonnes). Un nombre est codé sur 18 bits. Les registres sont reliés à un ensemble de circuits arithmétiques, des additionneurs et des multiplieurs. Le circuit supporte des instructions de copie entre registres, pour copier une ligne d'une matrice à une autre, des instructions LOAD/STORE pour lire ou écrire dans la mémoire RAM, etc. Il supporte aussi des multiplications en 2D et 3D. Le ''clipping divider'' est un circuit assez complexe, contenant un processeur à accumulateur, une mémoire ROM pour le programme du processeur. Le programme exécuté par le processeur est un petit programme de 62 instructions, stocké dans la ROM. L'algorithme du ''clipping divider'' est décrite dans le papier de recherche "A clipping divider", écrit par Robert Sproull. Un détail assez intéressant est que le résultat en sortie de l'unité géométrique et du rastériseur peuvent être envoyés à l'ordinateur en parallèle du rendu. C'était très utile sur les anciens ordinateurs qui étaient connectés à plusieurs terminaux. Le LDS-1 calculait la géométrie et le rendu, et le tout pouvait petre envoyé à d'autres composants, comme des terminaux, une imprimante, etc. ===Les systèmes ultérieurs : rendu à triangles colorés et texturé=== Les systèmes précédents étaient très limités : ils calculaient la géométrie et n'avaient pas de ''framebuffer'', ni de tampon de profondeur, ni gestion de l'éclairage, ni quoique ce soit. De tels systèmes étaient donc des accélérateurs géométriques que de vrais systèmes graphiques complets, du fait de l'absence de ''framebuffer''. Ils étaient composés de processeurs spécialisés dans les calculs à virgule flottante, faisant des calculs géométriques, et éventuellement d'un processeur pour la rastérisation. La raison est que la RAM était très chère et que créer des circuits fixes étaient très chers et peu disponibles. Par contre, les processeurs à virgule flottante étaient peu chers et facile à trouver. Vers la fin des années 80, grâce à la baisse du prix de la RAM et la démocratisation des ASIC (des circuits fixes fait sur mesure), ajouter un ''framebuffer'' est est devenu possible. C'est alors que sont apparus les '''systèmes de rendu 3D de première génération'''. De tels systèmes ont permis d'implémenter le rendu à primitives colorées qu'on a vu il y a quelques chapitres, à savoir un rendu où les triangles sont coloriés avec une couleur unique. Les systèmes de première génération étaient simples : des processeurs pour le calcul de la géométrie, un circuit de rastérisation, une RAM pour le ''framebuffer'' et des ASIC servant de ROPs très simples. Il n'y avait pas d'élimination des pixels cachés, pas de textures, et encore moins d'éclairage par pixels. Le premier système de ce genre était le ''Shaded Picture System'', toujours par Evans & Sutherland. Il ne gérait pas la couleur et ne pouvait afficher que des images en noir et blanc, mais il gérait l'éclairage par sommet (''vertex lighting''). Il a rapidement été dépassé par les systèmes de l'entreprise ''Silicon Graphics Inc'' (SGI), ainsi que ceux de l'entreprise Apollo avec sa série Apollo DN. Les '''systèmes de seconde génération''' sont apparus vers la fin des années 80, et se distinguent des précédents par l'ajout un tampon de profondeur. Ils intègrent aussi des capacités d'éclairage par pixel, à savoir de l'éclairage plat, de Gouraud, voire de Phong ! Enfin, les '''systèmes de troisième génération''' ont acquis des capacités de placage de texture, que les systèmes précédents n'avaient pas. Ils ont aussi ajouté un support de l'antialiasing. Les systèmes SGI avec placage de texture ont déjà été abordé au chapitre précédent, dans la section sur les GPU en mode immédiat et à ''tile''. Aussi, nous ne reviendrons pas dessus. [[File:Evolution de l'architecture des premières cartes graphiques, dans les années 80-90.png|centre|vignette|upright=2.5|Evolution de l'architecture des premières cartes graphiques, dans les années 80-90]] Les systèmes de première, seconde et troisième génération avaient de nombreux points communs. En premier lieu, ils étaient fabriqués en connectant plusieurs cartes électroniques : une carte pour les calculs géométriques, une ou plusieurs cartes pour le reste du rendu graphique, une carte dédiée au VDC et avec un connecteur écran. Les transistors de l'époque n'étaient pas encore miniaturisés, ce qui fait que le système graphique ne pouvait pas tenir sur une seule carte électronique. Il n'y avait donc pas de carte graphique proprement dit, mais un équivalent éclaté sur plusieurs cartes électroniques. La carte pour la géométrie contenait typiquement une mémoire FIFO pour accumuler les commandes de rendu, un processeur de commande, et plusieurs processeurs géométriques. Les processeurs géométriques étaient parfois conçus sur mesure, comme l'a été le le ''Geometry Engine'' de SGI. Mais il est arrivé qu'ils utilisent des processeurs commerciaux comme le Weitek 3222, l'Intel i860, etc. Les processeurs pouvaient être placés en série ou en parallèle, comme expliqué dans le chapitre précédent. Le circuit de rastérisation était réalisé soit avec un processeur dédié, soit avec un circuit fixe, soit un mélange des deux. La rastérisation est en effet réalisée en plusieurs étapes, certaines peuvent être implémentées avec un processeur et d'autres avec des circuits fixes. Un point important est qu'à l'époque, le rendu n'utilisait pas que des triangles, mais des polygones en général. Ce n'est que par la suite que le rendu s'est focalisé sur les triangles et les ''quads'' (quadrilatères). Il arrivait que le système graphique gérait partiellement des polygones concaves, voire convexes. Sur les systèmes SGI, les calculs géométriques se faisaient avec des polygones, que la rastérisation découpait en triangles, le reste du rendu se faisait avec des triangles. Les stations de travail Apollo DN 10000VS découpaient les polygones en trapézoïdes orientés à l'horizontale, alignés avec des ''scanlines''. D'autres systèmes découpaient tout en triangle lors de l'étape géométrique ==Les précurseurs grand public : les bornes d'arcade== [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] L'accélération du rendu 3D sur les bornes d'arcade était déjà bien avancé dès les années 90. Les bornes d'arcade ont toujours été un segment haut de gamme de l'industrie du jeu vidéo, aussi ce n'est pas étonnant. Le prix d'une borne d'arcade dépassait facilement les 10 000 dollars pour les plus chères et une bonne partie du prix était celui du matériel informatique. Le matériel était donc très puissant et débordait de mémoire RAM comparé aux consoles de jeu et aux PC. La plupart des bornes d'arcade utilisaient du matériel standardisé entre plusieurs bornes. A l'intérieur d'une borne d'arcade se trouve une '''carte de borne d'arcade''' qui est une carte mère avec un ou plusieurs processeurs, de la RAM, une carte graphique, un VDC et pas mal d'autres matériels. La carte est reliée aux périphériques de la borne : joysticks, écran, pédales, le dispositif pour insérer les pièces afin de payer, le système sonore, etc. Le jeu utilisé pour la borne est placé dans une cartouche qui est insérée dans un connecteur spécialisé. Les cartes de bornes d'arcade étaient généralement assez complexes, elles avaient une grande taille et avaient plus de composants que les cartes mères de PC. Chaque carte contenait un grand nombre de chips pour la mémoire RAM et ROM, et il n'était pas rare d'avoir plusieurs processeurs sur une même carte. Et il n'était pas rare d'avoir trois à quatre cartes superposées dans une seule borne. Pour ceux qui veulent en savoir plus, Fabien Sanglard a publié gratuitement un livre sur le fonctionnement des cartes d'arcade CPS System, disponible via ce lien : [https://fabiensanglard.net/b/cpsb.pdf The book of CP System]. Les premières cartes graphiques des bornes d'arcade étaient des cartes graphiques 2D auxquelles on avait ajouté quelques fonctionnalités. Les sprites pouvaient être tournés, agrandit/réduits, ou déformés pour simuler de la perspective et faire de la fausse 3D. Par la suite, le vrai rendu 3D est apparu sur les bornes d'arcade. Dès 1988, la carte d'arcade Namco System 21 et Sega Model 1 géraient les calculs géométriques. Quelques années plus tard, les cartes graphiques se sont mises à supporter un éclairage de Gouraud et du placage de texture. Par exemple, le Namco System 22 et la Sega model 2 supportaient des textures 2D et comme le filtrage de texture (bilinéaire et trilinéaire), le mip-mapping, et quelques autres. Au passage, les cartes graphiques de la Namco System 22 étaient développées en partenariat avec Eans & Sutherland, qui avait commencé à se diversifier dans le marché grand public. Les cartes graphiques de l'époque faisaient les calculs géométriques sur plusieurs processeurs, généralement des processeurs de type DSP (des processeurs spécialisés dans le traitement de signal). Par exemple, la Namco System 2 utilisait 4 DSP de marque Texas Instruments TMS320C25, cadencés à 24,576 MHz. La carte d'arcade Sega Model 1 utilisait quant à elle un DSP spécialisé dans les calculs géométriques. Par la suite, les bornes d'arcade ont réutilisé le hardware des PC et autres consoles de jeux. ==La 3D sur les consoles de quatrième/cinquième génération== Les consoles avant la quatrième génération de console étaient des consoles purement 2D, sans circuits d'accélération 3D. Leur carte graphique était un simple VDC 2D, plus ou moins performant selon la console. Les premières consoles de jeu capables de rendu 3D par elles-mêmes sont les consoles dites de 5ème génération. Il y a diverses manières de classer les consoles en générations, la plus commune place la 3D à la 5ème génération, mais détailler ces controverses quant à ce classement nous amènerait trop loin. Les consoles de génération avaient une architecture assez différente des systèmes antérieurs. Les systèmes SGI et assimilés pouvaient se permettre de couter assez cher, d'utiliser beaucoup de circuits, de prendre beaucoup de place. Les bornes d'arcade sont aussi dans ce cas. Aussi, il n'était pas rare que les cartes 3D de l'époque tiennent sur plusieurs cartes électroniques séparées. Mais une console ne peut pas se permettre ce genre de folies. Aussi, les cartes 3D des consoles de l'époque tenaient dans un seul circuit intégré, comme il est d'usage de nos jours. La conséquence est que certains circuits étaient fortement simplifiés, sur les consoles de cinquième génération. Et cela a impacté l'architecture interne des GPU des consoles. Les systèmes SGI avaient plusieurs processeurs pour calculer la géométrie, couplés à plusieurs unités non-programmables pour les pixels/textures. Les cartes 3D des consoles gardaient cette organisation : processeurs pour la géométrie, circuits fixes pour le reste. Mais elles se débrouillaient souvent avec un seul processeur, voire aucun ! Dans ce dernier cas, la géométrie était calculée sur le processeur principal, le CPU. Les unités pour les pixels étaient aussi moins nombreuses, mais il y en avait plusieurs, pour profiter de l'amplification des pixels. : Les cartes 3D des consoles de jeu utilisaient le placage de texture inverse, avec quelques exceptions qui utilisaient le placage de texture direct. ===Le rendu 3D sur les consoles de quatrième génération : la SNES=== Plus haut, j'ai dit que les consoles de quatrième génération n'avaient pas de carte accélératrice 3D. Pourtant, elles ont connus quelques jeux en vraie 3D. La raison à cela est que la 3D était calculée par un GPU placé dans les cartouches du jeu ! Par exemple, les cartouches de Starfox et de Super Mario 2 contenaient un coprocesseur Super FX, qui gérait des calculs de rendu 2D/3D. En tout, il y a environ 16 coprocesseurs pour la SNES et on en trouve facilement la liste sur le net. La console était conçue pour, des pins sur les ports cartouches étaient prévues pour des fonctionnalités de cartouche annexes, dont ces coprocesseurs. Ces pins connectaient le coprocesseur au bus des entrées-sorties. Les coprocesseurs des cartouches de NES avaient souvent de la mémoire rien que pour eux, qui était intégrée dans la cartouche. Ceci étant dit, passons aux consoles de cinquième génération. ===La Nintendo 64 : un GPU avancé=== La Nintendo 64 avait le GPU le plus complexe comparé aux autres consoles, et dépassait même les cartes graphiques des PC. Il faut dire que son GPU a été conçu avec l'aide de l'entreprise SGI, dont on a vu les systèmes graphiques plus haut. Le GPU de la N64 incorporait une unité pour les calculs géométriques, un circuit de rasterisation, une unité de textures et un ROP final pour les calculs de transparence/brouillard/antialiasing, ainsi qu'un circuit pour gérer la profondeur des pixels. En somme, tout le pipeline graphique était implémenté dans le GPU de la Nintendo 64, chose très en avance sur son temps, comparé au PC ou aux autres consoles ! Le GPU est construit autour d'un processeur dédié aux calculs géométriques, le ''Reality Signal Processor'' (RSP), autour duquel on a ajouté des circuits pour le reste du pipeline graphique. L'unité de calcul géométrique est un processeur MIPS R4000, un processeur assez courant à l'époque, auquel on avait retiré quelques fonctionnalités inutiles pour le rendu 3D. Il était couplé à 4 KB de mémoire vidéo, ainsi qu'à 4 KB de mémoire ROM. Le reste du GPU était réalisé avec des circuits fixes. Un point intéressant est que le programme exécuté par le RSP pouvait être programmé ! Le RSP gérait déjà des espèces de proto-shaders, qui étaient appelés des ''[https://ultra64.ca/files/documentation/online-manuals/functions_reference_manual_2.0i/ucode/microcode.html micro-codes]'' dans la documentation de l'époque. La ROM associée au RSP mémorise cinq à sept programmes différents, aux fonctionnalités différentes. * Les microcodes gspFast3D et gspF3DNoN, implémentent un rendu 3D normal, avec des options de ''clipping'' différentes entre les deux. * Le microcode gspTurbo3D fait la même chose, mais avec moins de fonctionnalités et avec une précision réduite. Il ne gère pas le ''clipping'', l'éclairage par pixel, la correction de perspective, l'antialiasing et quelques autres fonctionnalités. Il gère cependant l'éclairage de Gouraud. Il utilise une ''display list'' simplifiée comparé aux deux microcodes précédents. * Le microcode gspZ-Sort effectue une pré-passe z, à savoir qu'il calcule le tampon de profondeur final de la scène 3D, sans rendre l'image. Cela sert à faire une élimination des pixels cachés parfaite, en logiciel. On calcule le tampon de profondeur pour déterminer quels pixels sont visibles, puis une seconde passe rend l'image en, rejetant les pixels non-visibles. * Le microcode gspSprite2D implémente un rendu 2D émulé : les sprites et arrière-plan sont des rectangles texturés. Le microcode gspS2DEX fait la même chose, mais sert à émuler le rendu de la SNES plus qu'autre chose. * Le microcode gspLine3D ne gére que des lignes, pas de triangles. Il sert pour du rendu en fil de fer. Ils géraient le rendu 3D de manière différente et avec une gestion des ressources différentes. Très peu de studios de jeu vidéo ont développé leur propre microcodes N64, car la documentation était mal faite, que Nintendo ne fournissait pas de support officiel pour cela, que les outils de développement ne permettaient pas de faire cela proprement et efficacement. ===La Playstation 1=== Sur la Playstation 1 le calcul de la géométrie était réalisé par le processeur, la carte graphique gérait tout le reste. Et la carte graphique était un circuit fixe spécialisé dans la rasterisation et le placage de textures. Elle utilisait, comme la Nintendo 64, le placage de texture inverse, qui est apparu ensuite sur les cartes graphiques. ===La 3DO et la Sega Saturn=== La Sega Saturn et la 3DO étaient les deux seules consoles à utiliser le rendu direct. La géométrie était calculée sur le processeur, même si les consoles utilisaient parfois un CPU dédié au calcul de la géométrie. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. La Sega Saturn incorpore trois processeurs et deux GPU. Les deux GPUs sont nommés le VDP1 et le VDP2. Le VDP1 s'occupe des textures et des sprites, le VDP2 s'occupe uniquement de l'arrière-plan et incorpore un VDC tout ce qu'il y a de plus simple. Ils ne gèrent pas du tout la géométrie, qui est calculée par les trois processeurs. Le troisième processeur, la Saturn Control Unit, est un processeur de type DSP, à savoir un processeur spécialisé dans le traitement de signal. Il est utilisé presque exclusivement pour accélérer les calculs géométriques. Il avait sa propre mémoire RAM dédiée, 32 KB de SRAM, soit une mémoire locale très rapide. Les transferts entre cette RAM et le reste de l'ordinateur était géré par un contrôleur DMA intégré dans le DSP. En somme, il s'agit d'une sorte de processeur spécialisé dans la géométrie, une sorte d'unité géométrique programmable. Mais la géométrie n'était pas forcément calculée que sur ce DSP, mais pouvait être prise en charge par les 3 CPU. ==L'historique des cartes graphiques des PC, avant l'arrivée de Direct X et Open Gl== Sur PC, l'évolution des cartes graphiques a eu du retard par rapport aux consoles. Les PC sont en effet des machines multi-usage, pour lesquelles le jeu vidéo était un cas d'utilisation parmi tant d'autres. Et les consoles étaient la plateforme principale pour jouer à des jeux vidéo, le jeu vidéo PC étant plus marginal. Mais cela ne veut pas dire que le jeu PC n'existait pas, loin de là ! Un problème pour les jeux PC était que l'écosystème des PC était aussi fragmenté en plusieurs machines différentes : machines Apple 1 et 2, ordinateurs Commdore et Amiga, IBM PC et dérivés, etc. Aussi, programmer des jeux PC n'était pas mince affaire, car les problèmes de compatibilité étaient légion. C'est seulement quand la plateforme x86 des IBM PC s'est démocratisée que l'informatique grand public s'est standardisée, réduisant fortement les problèmes de compatibilité. Mais cela n'a pas suffit, il a aussi fallu que les API 3D naissent. Les API 3D comme Direct X et Open GL sont absolument cruciales pour garantir la compatibilité entre plusieurs ordinateurs aux cartes graphiques différentes. Aussi, l'évolution des cartes graphiques pour PC s'est faite main dans la main avec l'évolution des API 3D. Les fonctionnalités des cartes graphiques ont évolué dans le temps, en suivant les évolutions des API 3D. Du moins dans les grandes lignes, car il est arrivé plusieurs fois que des fonctionnalités naissent sur les cartes graphiques, pour que les fabricants forcent la main de Microsoft ou d'Open GL pour les intégrer de force dans les API 3D. Passons. ===L'introduction des premiers jeux 3D : Quake et les drivers miniGL=== L'API OpenGL est née de la main de SGI, encore eux ! SGI avait créé l'API Iris GL pour ses stations de travail Iris Graphics. Iris GL a ensuite été libéré et est devenu le standard Open GL. Open GL existait déjà avant l'apparition des cartes accélératrices 3D. Il y a avait donc déjà un terreau que les programmeurs graphiques pouvaient utiliser. Mais Open GL était surtout utilisé pour des applications industrielles, médicales (imagerie), graphiques ou militaires, pas pour le jeu vidéo. Mais cela changea avec la sortie du jeu Quake, d'IdSoftware, en 1996. Quake pouvait fonctionner en rendu logiciel, mais le programmeur responsable du moteur 3D (le célébre John Carmack) ajouta une version OpenGL du jeu. Il faut dire que le jeu était programmé sur une station de travail compatible avec OpenGL, même si aucune carte accélératrice de l'époque ne supportait OpenGL. C'était là un choix qui se révéla visionnaire. En théorie, le rendu par OpenGL aurait dû se faire intégralement en logiciel, sauf sur quelques rares stations de travail adaptées. Mais les premières cartes graphiques étaient déjà dans les starting blocks. La toute première carte 3D pour PC est la '''Rendition Vérité V1000''', sortie en Septembre 1995, soit quelques mois avant l'arrivée de la Nintendo 64. La Rendition Vérité V1000 contenait un processeur MIPS cadencé à 25 MHz, 4 mébioctets de RAM, une ROM pour le BIOS, et un RAMDAC, rien de plus. C'était un vrai ordinateur complètement programmable de bout en bout, sans aucun circuit fixe. Les programmeurs ne pouvaient cependant pas utiliser cette programmabilité avec des ''shaders'', mais elle permettait à Rendition d'implémenter n'importe quelle API 3D, que ce soit OpenGL, DirectX ou même sa son API propriétaire. La Rendition Vérité avait de bonnes performances pour ce qui est de la géométrie, mais pas pour le reste. Réaliser la rastérisation et le placage de texture en logiciel n'est pas efficace, pareil pour les opérations de fin de pipeline comme l'antialiasing. Le manque d'unités fixes très rapides pour la rastérisation, le placage de texture ou les opérations de fin de pipeline était clairement un gros défaut. Mais la Rendition Vérité était un cas à part, une exception dans le paysage des cartes 3D de l'époque, qui ne faisait rien comme les autres. Les autres cartes graphiques, sorties peu après, étaient les Voodoo de 3dfx, les Riva TNT de NVIDIA, les Rage/3D d'ATI, la Virge/3D de S3, et la Matrox Mystique. Elles avaient choisit le compromis inverse de la Rendition Vérité V1000 : de bonnes performances pour le placage de textures et la rastérization, mais pas pour les calculs géométriques. Pour rappel, les systèmes professionnels et les consoles avaient des processeurs pour la géométrie, et des circuits fixes pour le reste. Les cartes graphiques de PC se passaient des processeurs pour la géométrie, les calculs géométriques étaient réalisés par le CPU. Les toutes premières cartes 3D pour PC contenaient seulement des circuits pour gérer les textures et des ROPs. Elle géraient le ''z-buffer'' en mémoire vidéo, ainsi que des effets de brouillard. Il n'y avait même pas de circuit pour la rastérisation, qui était faite en logiciel, avec les calculs géométriques. [[File:Architecture de base d'une carte 3D - 2.png|centre|vignette|upright=1.5|Carte 3D sans rasterization matérielle.]] Les cartes suivantes ajoutèrent une gestion des étapes de ''rasterization'' directement en matériel. Les cartes ATI rage 2, les Invention de chez Rendition, et d'autres cartes graphiques supportaient la rasterisation en hardware. [[File:Architecture de base d'une carte 3D - 3.png|centre|vignette|upright=1.5|Carte 3D avec gestion de la géométrie.]] Pour exploiter les unités de texture et le circuit de rastérisation, OpenGL et Direct 3D étaient partiellement implémentées en logiciel, car les cartes graphiques ne supportaient pas toutes les fonctionnalités de l'API. C'était l'époque du miniGL, des implémentations partielles d'OpenGL, fournies par les fabricants de cartes 3D, implémentées dans les pilotes de périphériques de ces dernières. Les fonctionnalités d'OpenGL implémentées dans ces pilotes étaient presque toutes exécutées en matériel, par la carte graphique. Avec l'évolution du matériel, les pilotes de périphériques devinrent de plus en plus complets, au point de devenir des implémentations totales d'OpenGL. Mais au-delà d'OpenGL, chaque fabricant de carte graphique avait sa propre API propriétaire, qui était gérée par leurs pilotes de périphériques (''drivers''). Par exemple, les premières cartes graphiques de 3dfx interactive, les fameuses voodoo, disposaient de leur propre API graphique, l'API Glide. Elle facilitait la gestion de la géométrie et des textures, ce qui collait bien avec l'architecture de ces cartes 3D. Mais ces API propriétaires tombèrent rapidement en désuétude avec l'évolution de DirectX et d'OpenGL. Direct X était une API dans l'ombre d'Open GL. La première version de Direct X qui supportait la 3D était DirectX 2.0 (juin 2, 1996), suivie rapidement par DirectX 3.0 (septembre 1996). Elles dataient d'avant le jeu Quake, et elles étaient très éloignées du hardware des premières cartes graphiques. Elles utilisaient un système d'''execute buffer'' pour communiquer avec la carte graphique, Microsoft espérait que le matériel 3D implémenterait ce genre de système. Ce qui ne fu pas le cas. Direct X 4.0 a été abandonné en cours de développement pour laisser à une version 5.0 assez semblable à la 2.0/3.0. Le mode de rendu laissait de côté les ''execute buffer'' pour coller un peu plus au hardware de l'époque. Mais rien de vraiment probant comparé à Open GL. Même Windows utilisait Open GL au lieu de Direct X maison... C'est avec Direct X 6.0 que Direct X est entré dans la cours des grands. Il gérait la plupart des technologies supportées par les cartes graphiques de l'époque. ===Le ''multi-texturing'' de l'époque Direct X 6.0 : combiner plusieurs textures=== Une technologie très importante standardisée par Dirext X 6 est la technique du '''''multi-texturing'''''. Avec ce qu'on a dit dans le chapitre précédent, vous pensez sans doute qu'il n'y a qu'une seule texture par objet, qui est plaquée sur sa surface. Mais divers effet graphiques demandent d'ajouter des textures par dessus d'autres textures. En général, elles servent pour ajouter des détails, du relief, sur une surface pré-existante. Un exemple intéressant vient des jeux de tir : ajouter des impacts de balles sur les murs. Pour cela, on plaque une texture d'impact de balle sur le mur, à la position du tir. Il s'agit là d'un exemple de '''''decals''''', des petites textures ajoutées sur les murs ou le sol, afin de simuler de la poussière, des impacts de balle, des craquelures, des fissures, des trous, etc. Les textures en question sont de petite taille et se superposent à une texture existante, plus grande. Rendre des ''decals'' demande de pouvoir superposer deux textures. Direct X 6.0 supportait l'application de plusieurs textures directement dans le matériel. La carte graphique devait être capable d'accéder à deux textures en même temps, ou du moins faire semblant que. Pour cela, elle doublaient les unités de texture et adaptaient les connexions entre unités de texture et mémoire vidéo. La mémoire vidéo devait être capable de gérer plusieurs accès mémoire en même temps et devait alors avoir un débit binaire élevé. [[File:Multitexturing.png|centre|vignette|upright=2|Multitexturing]] La carte graphique devait aussi gérer de quoi combiner deux textures entre elles. Par exemple, pour revenir sur l'exemple d'une texture d'impact de balle, il faut que la texture d'impact recouvre totalement la texture du mur. Dans ce cas, la combinaison est simple : la première texture remplace l'ancienne, là où elle est appliquée. Mais les cartes graphiques ont ajouté d'autres combinaisons possibles, par exemple additionner les deux textures entre elle, faire une moyenne des texels, etc. Les opérations pour combiner les textures était le fait de circuits appelés des '''''combiners'''''. Concrètement, les ''combiners'' sont de simples unités de calcul. Les ''conbiners'' ont beaucoup évolués dans le temps, mais les premières implémentation se limitaient à quelques opérations simples : addition, multiplication, superposition, interpolation. L'opération effectuer était envoyée au ''conbiner'' sur une entrée dédiée. [[File:Multitexturing avec combiners.png|centre|vignette|upright=2|Multitexturing avec combiners]] S'il y avait eu un seul ''conbiner'', le circuit de ''multitexturing'' aurait été simplement configurable. Mais dans la réalité, les premières cartes utilisant du ''multi-texturing'' utilisaient plusieurs ''combiners'' placés les uns à la suite des autres. L'implémentation des ''combiners'' retenue par Open Gl, et par le hardware des cartes graphiques, était la suivante. Les ''combiners'' étaient placés en série, l'un à la suite de l'autre, chacun combinant le résultat de l'étage précédent avec une texture. Le premier ''combiner'' gérait l'éclairage par sommet, afin de conserver un minimum de rétrocompatibilité. [[File:Texture combiners Open GL.png|centre|vignette|upright=2|Texture combiners Open GL]] Voici les opérations supportées par les ''combiners'' d'Open GL. Ils prennent en entrée le résultat de l'étage précédent et le combinent avec une texture lue depuis l'unité de texture. {|class="wikitable" |+ Opérations supportées par les ''combiners'' d'Open GL |- ! Replace | colspan="2" | Pixel provenant de l'unité de texture |- ! Addition | colspan="2" | Additionne l'entrée au texel lu. |- ! Modulate | colspan="2" | Multiplie l'entrée avec le texel lu |- ! Mélange (''blending'') | Moyenne pondérée des deux entrées, pondérée par la composante de transparence || La couleur de transparence du texel lu et de l'entrée sont multipliées. |- ! Decals | Moyenne pondérée des deux entrées, pondérée par la composante de transparence. || La transparence du résultat est celle de l'entrée. |} Il faut noter qu'un dernier étage de ''combiners'' s'occupait d'ajouter la couleur spéculaire et les effets de brouillards. Il était à part des autres et n'était pas configurable, c'était un étage fixe, qui était toujours présent, peu importe le nombre de textures utilisé. Il était parfois appelé le '''''combiner'' final''', terme que nous réutiliserons par la suite. Mine de rien, cela a rendu les cartes graphiques partiellement programmables. Le fait qu'il y ait des opérations enchainées à la suite, opérations qu'on peut choisir librement, suffit à créer une sorte de mini-programme qui décide comment mélanger plusieurs textures. Mais il y avait une limitation de taille : le fait que les données soient transmises d'un étage à l'autre, sans détours possibles. Par exemple, le troisième étage ne pouvait avoir comme seule opérande le résultat du second étage, mais ne pouvait pas utiliser celui du premier étage. Il n'y avait pas de registres pour stocker ce qui sortait de la rastérisation, ni pour mémoriser temporairement les texels lus. ===Le ''Transform & Lighting'' matériel de Direct X 7.0=== [[File:Architecture de base d'une carte 3D - 4.png|vignette|upright=1.5|Carte 3D avec gestion de la géométrie.]] La première carte graphique pour PC capable de gérer la géométrie en hardware fût la Geforce 256, la toute première Geforce. Son unité de gestion de la géométrie n'est autre que la bien connue '''unité T&L''' (''Transform And Lighting''). Elle implémentait des algorithmes d'éclairage de la scène 3D assez simples, comme un éclairage de Gouraud, qui étaient directement câblés dans ses circuits. Mais contrairement à la Nintendo 64 et aux bornes d'arcade, elle implémentait le tout, non pas avec un processeur classique, mais avec des circuits fixes. Avec Direct X 7.0 et Open GL 1.0, l'éclairage était en théorie limité à de l'éclairage par sommet, l'éclairage par pixel n'était pas implémentable en hardware. Les cartes graphiques ont tenté d'implémenter l'éclairage par pixel, mais cela n'est pas allé au-delà du support de quelques techniques de ''bump-mapping'' très limitées. Par exemple, Direct X 6.0 implémentait une forme limitée de ''bump-mapping'', guère plus. Un autre problème est qu'il a beaucoup d'algorithmes d'éclairages différents, aux résultats visuels différents, bien au-delà des algorithmes d'éclairage plat, de Gouraud et de Phong. Et les unités de T&L étaient souvent en retard sur les algorithmes logiciels. Les programmeurs avaient le choix entre programmer les algorithmes d’éclairage qu'ils voulaient et les exécuter en logiciel, ou utiliser ceux de l'unité de T&L. Ils choisissaient souvent la première option. Par exemple, Quake 3 Arena et Unreal Tournament n'utilisaient pas les capacités d'éclairage géométrique et préféraient utiliser leurs calculs d'éclairage logiciel fait maison. Cependant, le hardware dépassait les capacités des API et avait déjà commencé à ajouter des capacités de programmation liées au ''multi-texturing''. Les cartes graphiques de l'époque, surtout chez NVIDIA, implémentaient un système de '''''register combiners''''', une forme améliorée de ''texture combiners'', qui permettait de faire une forme limitée d'éclairage par pixel, notamment du vrai ''bump-mampping'', voire du ''normal-mapping''. Mais ce n'était pas totalement supporté par les API 3D de l'époque. Les ''registers combiners'' sont des ''texture combiners'' mais dans lesquels ont aurait retiré la stricte organisation en série. Il y a toujours plusieurs étages à la suite, qui peuvent exécuter chacun une opération, mais tous les étages ont maintenant accès à toutes les textures lues et à tout ce qui sort de la rastérisation, pas seulement au résultat de l'étape précédente. Pour cela, on ajoute des registres pour mémoriser ce qui sort des unités de texture, et pour ce qui sort de la rastérisation. De plus, on ajoute des registres temporaires pour mémoriser les résultats de chaque ''combiner'', de chaque étage. Il faut cependant signaler qu'il existe un ''combiner'' final, séparé des étages qui effectuent des opérations proprement dits. Il s'agit de l'étage qui applique la couleur spéculaire et les effets de brouillards. Il ne peut être utilisé qu'à la toute fin du traitement, en tant que dernier étage, on ne peut pas mettre d'opérations après lui. Sa sortie est directement connectée aux ROPs, pas à des registres. Il faut donc faire la distinction entre les '''''combiners'' généraux''' qui effectuent une opération et mémorisent le résultat dans des registres, et le ''combiner'' final qui envoie le résultat aux ROPs. L'implémentation des ''register combiners'' utilisait un processeur spécialisés dans les traitements sur des pixels, une sorte de proto-processeur de ''shader''. Le processeur supportait des opérations assez complexes : multiplication, produit scalaire, additions. Il s'agissait d'un processeur de type VLIW, qui sera décrit dans quelques chapitres. Mais ce processeur avait des programmes très courts. Les premières cartes NVIDIA, comme les cartes TNT pouvaient exécuter deux opérations à la suite, suivie par l'application de la couleurs spéculaire et du brouillard. En somme, elles étaient limitées à un ''shader'' à deux/trois opérations, mais c'était un début. Le nombre d'opérations consécutives est rapidement passé à 8 sur la Geforce 3. ===L'arrivée des ''shaders'' avec Direct X 8.0=== [[File:Architecture de la Geforce 3.png|vignette|upright=1.5|Architecture de la Geforce 3]] Les ''register combiners'' était un premier pas vers un éclairage programmable. Paradoxalement, l'évolution suivante s'est faite non pas dans l'unité de rastérisation/texture, mais dans l'unité de traitement de la géométrie. La Geforce 3 a remplacé l'unité de T&L par un processeur capable d'exécuter des programmes. Les programmes en question complétaient l'unité de T&L, afin de pouvoir rajouter des techniques d'éclairage plus complexes. Le tout a permis aussi d'ajouter des animations, des effets de fourrures, des ombres par ''shadow volume'', des systèmes de particule évolués, et bien d'autres. À partir de la Geforce 3 de Nvidia, les cartes graphiques sont devenues capables d'exécuter des programmes appelés '''''shaders'''''. Le terme ''shader'' vient de ''shading'' : ombrage en anglais. Grace aux ''shaders'', l'éclairage est devenu programmable, il n'est plus géré par des unités d'éclairage fixes mais été laissé à la créativité des programmeurs. Les programmeurs ne sont plus vraiment limités par les algorithmes d'éclairage implémentés dans les cartes graphiques, mais peuvent implémenter les algorithmes d'éclairage qu'ils veulent et peuvent le faire exécuter directement sur la carte graphique. Les ''shaders'' sont classifiés suivant les données qu'ils manipulent : '''''pixel shader''''' pour ceux qui manipulent des pixels, '''''vertex shaders''''' pour ceux qui manipulent des sommets. Les premiers sont utilisés pour implémenter l'éclairage par pixel, les autres pour gérer tout ce qui a trait à la géométrie, pas seulement l'éclairage par sommets. Direct X 8.0 avait un standard pour les shaders, appelé ''shaders 1.0'', qui correspondait parfaitement à ce dont était capable la Geforce 3. Il standardisait les ''vertex shaders'' de la Geforce 3, mais il a aussi renommé les ''register combiners'' comme étant des ''pixel shaders'' version 1.0. Les ''register combiners'' n'ont pas évolués depuis la Geforce 256, si ce n'est que les programmes sont passés de deux opérations successives à 8, et qu'il y avait possibilité de lire 4 textures en ''multitexturing''. A l'opposé, le processeur de ''vertex shader'' de la Geforce 3 était capable d'exécuter des programmes de 128 opérations consécutives et avait 258 registres différents ! Des ''pixels shaders'' plus évolués sont arrivés avec l'ATI Radeon 8500 et ses dérivés. Elle incorporait la technologie ''SMARTSHADER'' qui remplacait les ''registers combiners'' par un processeur de ''shader'' un peu limité. Un point est que le processeur acceptait de calculer des adresses de texture dans le ''pixel shader''. Avant, les adresses des texels à lire étaient fournis par l'unité de rastérisation et basta. L'avantage est que certains effets graphiques étaient devenus possibles : du ''bump-mapping'' avancé, des textures procédurales, de l'éclairage par pixel anisotrope, du éclairage de Phong réel, etc. Avec la Radeon 8500, le ''pixel shader'' pouvait calculer des adresses, et lire les texels associés à ces adresses calculées. Les ''pixel shaders'' pouvaient lire 6 textures, faire 8 opérations sur les texels lus, puis lire 6 textures avec les adresses calculées à l'étape précédente, et refaire 8 opérations. Quelque chose de limité, donc, mais déjà plus pratique. Les ''pixel shaders'' de ce type ont été standardisé dans Direct X 8.1, sous le nom de ''pixel shaders 1.4''. Encore une fois, le hardware a forcé l'intégration dans une API 3D. ===Les ''shaders'' de Direct X 9.0 : de vrais ''pixel shaders''=== Avec Direct X 9.0, les ''shaders'' sont devenus de vrais programmes, sans les limitations des ''shaders'' précédents. Les ''pixels shaders'' sont passés à la version 2.0, idem pour les ''vertex shaders''. Concrètement, ils ont des fonctionnalités bien supérieures à celles des ''registers combiners''. Les ''shaders'' pouvaient exécuter une suite d'opérations arbitraire, dans le sens où elle n'était pas structurée avec tel type d'opération au début, suivie par un accès aux textures, etc. On pouvait mettre n'importe quelle opération dans n'importe quel ordre. De plus, les ''shaders'' ne sont plus écrit en assembleur comme c'était le cas avant. Ils sont dorénavant écrits dans un langage de haut-niveau, le HLSL pour les shaders Direct X et le GLSL pour les shaders Open Gl. Les ''shaders'' sont ensuite traduit (compilés) en instructions machines compréhensibles par la carte graphique. Au début, ces langages et la carte graphique supportaient uniquement des opérations simples. Mais au fil du temps, les spécifications de ces langages sont devenues de plus en plus riches à chaque version de Direct X ou d'Open Gl, et le matériel en a fait autant. Le matériel s'est alors adapté, en incorporant un véritable processeur pour les ''pixel shaders''. Les ''pixel shaders'' sont maintenant exécutés par un processeur de ''shader'' dédié, aux fonctionnalités bien supérieures à celles des ''registers combiners''. Le processeur de ''pixel shader'' incorpore l'unité de texture en sont sein, les deux sont fusionnés. La raison à cela sera expliqué dans la suite du chapitre. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=1.5|Carte 3D avec pixels et vertex shaders non-unifiés.]] ===L'après Direct X 9.0=== Avant Direct X 10, les processeurs de ''shaders'' ne géraient pas exactement les mêmes opérations pour les processeurs de ''vertex shader'' et de ''pixel shader''. Les processeurs de ''vertex shader'' et de ''pixel shader''étaient séparés. Depuis DirectX 10, ce n'est plus le cas : le jeu d'instructions a été unifié entre les vertex shaders et les pixels shaders, ce qui fait qu'il n'y a plus de distinction entre processeurs de vertex shaders et de pixels shaders, chaque processeur pouvant traiter indifféremment l'un ou l'autre. [[File:Architecture de base d'une carte 3D - 6.png|centre|vignette|upright=1.5|Architecture de la GeForce 6800.]] Avec Direct X 10, de nombreux autres ''shaders'' sont apparus. Les plus liés au rendu 3D sont les '''''geometry shader''''' pour ceux qui manipulent des triangles, de ''hull shaders'' et de ''domain shaders'' pour la tesselation. De plus, les cartes graphiques modernes sont capables d’exécuter des programmes informatiques qui n'ont aucun lien avec le rendu 3D, mais sont exécutés par la carte graphique comme le ferait un processeur d'ordinateur normal. De tels ''shaders'' sans lien avec le rendu 3D sont appelés des ''compute shader''. ==Les cartes graphiques d'aujourd'hui== Avec l'arrivée des shaders, les circuits d'une carte graphique sont divisés en deux catégories : d'un côté les circuits non-programmables et de l'autre les circuits programmables. Pour exécuter les ''shaders'', la carte graphique incorpore des '''processeurs de ''shaders''''', des processeurs similaires aux processeurs des ordinateurs, aux CPU, mais avec quelques petites différences qu'on expliquera dans le prochain chapitre. A côté des processeurs de ''shaders'', il reste quelques circuits non-programmables appelés des circuits fixes. De nos jours, la gestion de la géométrie et des pixels est programmable, mais la rastérisation, le placage de texture, le ''culling'' et l'enregistrement du ''framebuffer'' ne l'est pas. Il n'en a pas toujours été ainsi. [[File:3D-Pipeline.svg|centre|vignette|upright=3.0|Pipeline 3D : ce qui est programmable et ce qui ne l'est pas dans une carte graphique moderne.]] Une carte graphique contient donc un mélange de circuits fixes et de processeurs de ''shaders'', qui peut sembler contradictoire. Pourquoi ne pas tout rendre programmable ? Ou au contraire, utiliser seulement des circuits fixes ? La réponse rapide est qu'il s'agit d'un compromis entre flexibilité et performance qui permet d'avoir le meilleur des deux mondes. Mais ce compromis a fortement évolué dans le temps, comme on va le voir plus bas. Rendre la gestion de la géométrie ou des pixels programmable permet d'implémenter facilement un grand nombre d'effets graphiques sans avoir à les implémenter en hardware. Avant les ''shaders'', seul le hardware récent gérait les dernières fonctionnalités. Les effets graphiques derniers cri n'étaient disponibles que sur les derniers modèles de carte graphique. Avec des ''vertex/pixel shaders'', ce genre de défaut est passé à la trappe. Si un nouvel algorithme de rendu graphique est inventé, il peut être utilisé dès le lendemain sur toutes les cartes graphiques modernes. De plus, implémenter beaucoup d'algorithmes d'éclairage différents est difficile. Le cout en termes de transistors et de complexité était assez important, utiliser des circuits programmable a un cout en hardware plus limité. Tout cela est à l'exact opposé de ce qu'on a avec les autres circuits, comme les circuits pour la rastérisation ou le placage de texture. Il n'y a pas 36 façons de rastériser une scène 3D et la flexibilité n'est pas un besoin important pour cette opération, alors que les performances sont cruciales. Même chose pour le placage/filtrage de textures. En conséquences, les unités de transformation, de rastérisation et de placage de texture sont toutes implémentées en matériel. Faire ainsi permet de gagner en performance sans que cela ait le moindre impact pour le programmeur. ===Les unités de texture sont intégrées aux processeurs de shaders=== Avec l'arrivée des processeurs de shaders, les unités de texture ont été intégrées dans les processeurs de shaders eux-mêmes. C'est la seule unité fixe qui a subit ce traitement, et il est intéressant de comprendre pourquoi. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=2|Architecture de base d'une carte 3D.]] Pour cela, il faut faire un rappel sur ce qu'il y a dans un processeur. Un processeur contient globalement quatre circuits : * une unité de calcul qui fait des calculs ; * des registres pour stocker les opérandes et résultats des calculs ; * une unité de communication avec la mémoire ; * et un séquenceur, un circuit de contrôle qui commande les autres. L'unité de communication avec la mémoire sert à lire ou écrire des données, à les transférer de la RAM vers les registres, ou l'inverse. Lire une donnée demande d'envoyer son adresse à la RAM, qui répond en envoyant la donnée lue. Elle est donc toute indiquée pour lire une texture : lire une texture n'est qu'un cas particulier de lecture de données. Les texels à lire sont à une adresse précise, la RAM répond à la lecture avec le texel demandé. Il est donc possible d'utiliser l'unité de communication avec la mémoire comme si c'était une unité de texture. Cependant, les textures ne sont pas utilisées comme telles de nos jours. Le rendu 3D moderne utilise des techniques dites de filtrage de texture, qui permettent d'améliorer la qualité du rendu des textures. Sans ce filtrage de texture, les textures appliquées naïvement donnent un résultat assez pixelisé et assez moche, pour des raisons assez techniques. Le filtrage élimine ces artefacts, en utilisant une forme d'''antialiasing'' interne aux textures, le fameux filtrage de texture. Le filtrage de texture peut être réalisé en logiciel ou en matériel. Techniquement, il est possible de le faire dans un ''shader''. Le ''shader'' calcule les adresses des texels à lire, lit les texels, et effectue ensuite le filtrage avec des opérations de calcul. Mais ce n'est pas ce qui est fait, le filtrage de texture est toujours effectué directement en matériel. La raison est que le filtrage de texture est très simple à implémenter en hardware. Le filtrage bilinéaire ou trilinéaire demande juste des circuits d'interpolation et quelques registres, ce qui est trivial. Et la seconde raison est qu'il n'y a pas 36 façons de filtrer des textures : une carte graphique peut implémenter les algorithmes principaux existants en assez peu de circuits. Pour simplifier l'implémentation, les processeurs de ''shader'' modernes disposent d'une unité d'accès mémoire séparée de l'unité de texture. L'unité d'accès mémoire normale s'occupe des accès mémoire hors-textures, alors que l'unité mémoire s'occupe de lire les textures. L'unité de texture contient de quoi faire du filtrage de texture, mais aussi faire des calculs d'adresse spécialisées, intrinsèquement liés au format des textures, qu'on détaillera dans le chapitre sur les textures. En comparaison, les unités d'accès mémoire effectuent des calculs d'adresse plus basiques. Un dernier avantage est que l'unité de texture est reliée au cache de texture, alors que l'unité d'accès mémoire est relié au cache L1/L2. : Il faut noter que les ROPs peuvent aussi être intégré dans les processeurs de ''shader'', mais c'est assez rare. Les cartes graphiques pour PC ont des ROPs séparés des processeurs de ''shaders''. Par contre, quelques cartes graphiques destinées les smartphones et autres appareils mobiles font exception. Sur celles-ci, les unités de textures et les ROPs sont intégrés dans les processeurs de ''shaders'', aux côtés de l'unité d'accès mémoire LOAD/STORE. Il s'agit de cartes graphiques en rendu à tuiles. {{NavChapitre | book=Les cartes graphiques | prev=Les cartes graphiques : architecture de base | prevText=Les cartes graphiques : architecture de base | next=Les processeurs de shaders | nextText=Les processeurs de shaders }} {{autocat}} 848b7lyzowdnmed9oq0mp8mstc6itd8 763502 763501 2026-04-11T19:29:59Z Mewtow 31375 /* Les cartes graphiques d'aujourd'hui */ 763502 wikitext text/x-wiki Il est intéressant d'étudier le hardware des cartes graphiques en faisant un petit résumé de leur évolution dans le temps. En effet, leur hardware a fortement évolué dans le temps. Et il serait difficile à comprendre le hardware actuel sans parler du hardware d'antan. En effet, une carte graphique moderne est partiellement programmable. Certains circuits sont totalement programmables, d'autres non. Et pour comprendre pourquoi, il faut étudier comment ces circuits ont évolués. Le hardware des cartes graphiques a fortement évolué dans le temps, ce qui n'est pas une surprise. Les évolutions de la technologie, avec la miniaturisation des transistors et l'augmentation de leurs performances a permis aux cartes graphiques d'incorporer de plus en plus de circuits avec les années. Avant l'invention des cartes graphiques, toutes les étapes du pipeline graphique étaient réalisées par le processeur : il calculait l'image à afficher, et l’envoyait à une carte d'affichage 2D. Au fil du temps, de nombreux circuits furent ajoutés, afin de déporter un maximum de calculs vers la carte vidéo. Le rendu 3D moderne est basé sur le placage de texture inverse, avec des coordonnées de texture, une correction de perspective, etc. Mais les anciennes consoles et bornes d'arcade utilisaient le placage de texture direct. Et cela a impacté le hardware des consoles/PCs de l'époque. Avec le placage de texture direct, il était primordial de calculer la géométrie, mais la rasterisation était le fait de VDC améliorés. Aussi, les premières bornes d'arcade 3D et les consoles de 5ème génération disposaient processeurs pour calculer la géométrie et de circuits d'application de textures très particuliers. A l'inverse, les PC utilisaient un rendu inverse, totalement différent. Sur les PC, les premières cartes graphiques avaient un circuit de rastérisation et des unités de textures, mais pas de circuits géométriques. ==Les premières cartes graphiques== Dès les années 70-80, le rendu 3D était utilisé par de nombreuses entreprises industrielles : des applications de visualisation 3D étaient utilisées en architecture, des applications de conception assistée par ordinateur étaient déjà d'utilisation courante, sans compter les simulateurs de vol utilisés par l'armée et les instructeurs qui formaient les pilotes d'avion. Le rendu 3D était aussi étudié au niveau académique, la recherche en 3D était déjà florissante. Il existait même du matériel spécifiquement conçu pour le rendu graphique, mais celui-ci était spécifiquement dédié à des super-calculateurs ou des ''workstations'' (des sortes d'ancêtres des PC, très puissants pour l'époque, mais conçus uniquement pour les entreprises). ===Le début des années 80 : le rendu en fils de fer=== Le tout premier système de ce genre était le '''''Line Drawing System-1''''' de l'entreprise Evans & Sutherland, daté de 1969. Ce n'est ni plus ni moins que le toute premier circuit graphique séparé du processeur ayant existé. C'est en un sens la toute première carte graphique, le tout premier GPU. Il prenait la forme d'un périphérique qui se connectait à l'ordinateur d'un côté et était relié à l'écran de l'autre. Il était compatible avec un grand nombre d'ordinateurs et de processeurs existants. Il a été suivi par plusieurs successeurs, nommés ''Picture System 1, 2'' et le ''PS300 series''. [[File:Evans & Sutherland LDS-1 (1).jpg|vignette|Evans & Sutherland LDS-1 (1)]] Ils permettaient de faire du rendu en fil de fer, sans texture ni même sans polygones colorés. Un tel rendu était utile pour des applications assez limitées : architecture, dessin de molécules pour les entreprises pharmaceutique et certains centres de recherche, l'aérospatiale, etc. Ces cartes graphiques étaient utilisées de concert avec des écrans appelés '''écrans vectoriels''' (''vector display''). Pour simplifier, ils ressemblaient à des écrans CRT, sauf que le faisceau d'électron ne balayait pas l'écran ligne par ligne, mais traçait des lignes arbitraires à l'écran. On lui précisait deux points de coordonnées x1,y1 ; et x2,y2 ; puis l'écran tracait une ligne entre ces deux points. En général, la ligne tracée était maintenue pendant un long moment, entre plusieurs secondes et plusieurs minutes. L'intérieur du circuit était assez simple : un circuit de multiplication de matrice pour les calculs géométriques, un rastériser simplifié (le ''clipping diviser''), un circuit de tracé de lignes, et un processeur de contrôle pour commander les autres circuits. Le fait que ces trois circuits soient séparés permettait une implémentation en pipeline, où plusieurs portions de l'image pouvaient être calculées en même temps : pendant que l'une est dans l'unité géométrique, l'autre est dans le rastériseur et une troisième est en cours de tracé. [[File:Lds1blockdiagram05.svg|centre|vignette|upright=2|Architecture du LDS-1. Le processeur de contrôle n'est pas représenté.]] Le processeur de contrôle exécute un programme qui se charge de commander l'unité géométrique et les autres circuits. Le programme en question est fourni par le programmeur, le LDS-1 est donc totalement programmable. Il lit directement les données nécessaires pour le rendu dans la mémoire de l’ordinateur et le programme exécuté est lui aussi en mémoire principale. Il n'a pas de mémoire vidéo dédiée, il utilise la RAM de l'ordinateur principal. Le multiplieur de matrices est plus complexe qu'on pourrait s'y attendre. Il ne s'agit pas que d'un circuit arithmétique tout simple, mais d'un véritable processeur avec des registres et des instructions machine complexes. Il contient plusieurs registres, l'ensemble mémorisant 4 matrices de 16 nombres chacune (4 lignes de 4 colonnes). Un nombre est codé sur 18 bits. Les registres sont reliés à un ensemble de circuits arithmétiques, des additionneurs et des multiplieurs. Le circuit supporte des instructions de copie entre registres, pour copier une ligne d'une matrice à une autre, des instructions LOAD/STORE pour lire ou écrire dans la mémoire RAM, etc. Il supporte aussi des multiplications en 2D et 3D. Le ''clipping divider'' est un circuit assez complexe, contenant un processeur à accumulateur, une mémoire ROM pour le programme du processeur. Le programme exécuté par le processeur est un petit programme de 62 instructions, stocké dans la ROM. L'algorithme du ''clipping divider'' est décrite dans le papier de recherche "A clipping divider", écrit par Robert Sproull. Un détail assez intéressant est que le résultat en sortie de l'unité géométrique et du rastériseur peuvent être envoyés à l'ordinateur en parallèle du rendu. C'était très utile sur les anciens ordinateurs qui étaient connectés à plusieurs terminaux. Le LDS-1 calculait la géométrie et le rendu, et le tout pouvait petre envoyé à d'autres composants, comme des terminaux, une imprimante, etc. ===Les systèmes ultérieurs : rendu à triangles colorés et texturé=== Les systèmes précédents étaient très limités : ils calculaient la géométrie et n'avaient pas de ''framebuffer'', ni de tampon de profondeur, ni gestion de l'éclairage, ni quoique ce soit. De tels systèmes étaient donc des accélérateurs géométriques que de vrais systèmes graphiques complets, du fait de l'absence de ''framebuffer''. Ils étaient composés de processeurs spécialisés dans les calculs à virgule flottante, faisant des calculs géométriques, et éventuellement d'un processeur pour la rastérisation. La raison est que la RAM était très chère et que créer des circuits fixes étaient très chers et peu disponibles. Par contre, les processeurs à virgule flottante étaient peu chers et facile à trouver. Vers la fin des années 80, grâce à la baisse du prix de la RAM et la démocratisation des ASIC (des circuits fixes fait sur mesure), ajouter un ''framebuffer'' est est devenu possible. C'est alors que sont apparus les '''systèmes de rendu 3D de première génération'''. De tels systèmes ont permis d'implémenter le rendu à primitives colorées qu'on a vu il y a quelques chapitres, à savoir un rendu où les triangles sont coloriés avec une couleur unique. Les systèmes de première génération étaient simples : des processeurs pour le calcul de la géométrie, un circuit de rastérisation, une RAM pour le ''framebuffer'' et des ASIC servant de ROPs très simples. Il n'y avait pas d'élimination des pixels cachés, pas de textures, et encore moins d'éclairage par pixels. Le premier système de ce genre était le ''Shaded Picture System'', toujours par Evans & Sutherland. Il ne gérait pas la couleur et ne pouvait afficher que des images en noir et blanc, mais il gérait l'éclairage par sommet (''vertex lighting''). Il a rapidement été dépassé par les systèmes de l'entreprise ''Silicon Graphics Inc'' (SGI), ainsi que ceux de l'entreprise Apollo avec sa série Apollo DN. Les '''systèmes de seconde génération''' sont apparus vers la fin des années 80, et se distinguent des précédents par l'ajout un tampon de profondeur. Ils intègrent aussi des capacités d'éclairage par pixel, à savoir de l'éclairage plat, de Gouraud, voire de Phong ! Enfin, les '''systèmes de troisième génération''' ont acquis des capacités de placage de texture, que les systèmes précédents n'avaient pas. Ils ont aussi ajouté un support de l'antialiasing. Les systèmes SGI avec placage de texture ont déjà été abordé au chapitre précédent, dans la section sur les GPU en mode immédiat et à ''tile''. Aussi, nous ne reviendrons pas dessus. [[File:Evolution de l'architecture des premières cartes graphiques, dans les années 80-90.png|centre|vignette|upright=2.5|Evolution de l'architecture des premières cartes graphiques, dans les années 80-90]] Les systèmes de première, seconde et troisième génération avaient de nombreux points communs. En premier lieu, ils étaient fabriqués en connectant plusieurs cartes électroniques : une carte pour les calculs géométriques, une ou plusieurs cartes pour le reste du rendu graphique, une carte dédiée au VDC et avec un connecteur écran. Les transistors de l'époque n'étaient pas encore miniaturisés, ce qui fait que le système graphique ne pouvait pas tenir sur une seule carte électronique. Il n'y avait donc pas de carte graphique proprement dit, mais un équivalent éclaté sur plusieurs cartes électroniques. La carte pour la géométrie contenait typiquement une mémoire FIFO pour accumuler les commandes de rendu, un processeur de commande, et plusieurs processeurs géométriques. Les processeurs géométriques étaient parfois conçus sur mesure, comme l'a été le le ''Geometry Engine'' de SGI. Mais il est arrivé qu'ils utilisent des processeurs commerciaux comme le Weitek 3222, l'Intel i860, etc. Les processeurs pouvaient être placés en série ou en parallèle, comme expliqué dans le chapitre précédent. Le circuit de rastérisation était réalisé soit avec un processeur dédié, soit avec un circuit fixe, soit un mélange des deux. La rastérisation est en effet réalisée en plusieurs étapes, certaines peuvent être implémentées avec un processeur et d'autres avec des circuits fixes. Un point important est qu'à l'époque, le rendu n'utilisait pas que des triangles, mais des polygones en général. Ce n'est que par la suite que le rendu s'est focalisé sur les triangles et les ''quads'' (quadrilatères). Il arrivait que le système graphique gérait partiellement des polygones concaves, voire convexes. Sur les systèmes SGI, les calculs géométriques se faisaient avec des polygones, que la rastérisation découpait en triangles, le reste du rendu se faisait avec des triangles. Les stations de travail Apollo DN 10000VS découpaient les polygones en trapézoïdes orientés à l'horizontale, alignés avec des ''scanlines''. D'autres systèmes découpaient tout en triangle lors de l'étape géométrique ==Les précurseurs grand public : les bornes d'arcade== [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] L'accélération du rendu 3D sur les bornes d'arcade était déjà bien avancé dès les années 90. Les bornes d'arcade ont toujours été un segment haut de gamme de l'industrie du jeu vidéo, aussi ce n'est pas étonnant. Le prix d'une borne d'arcade dépassait facilement les 10 000 dollars pour les plus chères et une bonne partie du prix était celui du matériel informatique. Le matériel était donc très puissant et débordait de mémoire RAM comparé aux consoles de jeu et aux PC. La plupart des bornes d'arcade utilisaient du matériel standardisé entre plusieurs bornes. A l'intérieur d'une borne d'arcade se trouve une '''carte de borne d'arcade''' qui est une carte mère avec un ou plusieurs processeurs, de la RAM, une carte graphique, un VDC et pas mal d'autres matériels. La carte est reliée aux périphériques de la borne : joysticks, écran, pédales, le dispositif pour insérer les pièces afin de payer, le système sonore, etc. Le jeu utilisé pour la borne est placé dans une cartouche qui est insérée dans un connecteur spécialisé. Les cartes de bornes d'arcade étaient généralement assez complexes, elles avaient une grande taille et avaient plus de composants que les cartes mères de PC. Chaque carte contenait un grand nombre de chips pour la mémoire RAM et ROM, et il n'était pas rare d'avoir plusieurs processeurs sur une même carte. Et il n'était pas rare d'avoir trois à quatre cartes superposées dans une seule borne. Pour ceux qui veulent en savoir plus, Fabien Sanglard a publié gratuitement un livre sur le fonctionnement des cartes d'arcade CPS System, disponible via ce lien : [https://fabiensanglard.net/b/cpsb.pdf The book of CP System]. Les premières cartes graphiques des bornes d'arcade étaient des cartes graphiques 2D auxquelles on avait ajouté quelques fonctionnalités. Les sprites pouvaient être tournés, agrandit/réduits, ou déformés pour simuler de la perspective et faire de la fausse 3D. Par la suite, le vrai rendu 3D est apparu sur les bornes d'arcade. Dès 1988, la carte d'arcade Namco System 21 et Sega Model 1 géraient les calculs géométriques. Quelques années plus tard, les cartes graphiques se sont mises à supporter un éclairage de Gouraud et du placage de texture. Par exemple, le Namco System 22 et la Sega model 2 supportaient des textures 2D et comme le filtrage de texture (bilinéaire et trilinéaire), le mip-mapping, et quelques autres. Au passage, les cartes graphiques de la Namco System 22 étaient développées en partenariat avec Eans & Sutherland, qui avait commencé à se diversifier dans le marché grand public. Les cartes graphiques de l'époque faisaient les calculs géométriques sur plusieurs processeurs, généralement des processeurs de type DSP (des processeurs spécialisés dans le traitement de signal). Par exemple, la Namco System 2 utilisait 4 DSP de marque Texas Instruments TMS320C25, cadencés à 24,576 MHz. La carte d'arcade Sega Model 1 utilisait quant à elle un DSP spécialisé dans les calculs géométriques. Par la suite, les bornes d'arcade ont réutilisé le hardware des PC et autres consoles de jeux. ==La 3D sur les consoles de quatrième/cinquième génération== Les consoles avant la quatrième génération de console étaient des consoles purement 2D, sans circuits d'accélération 3D. Leur carte graphique était un simple VDC 2D, plus ou moins performant selon la console. Les premières consoles de jeu capables de rendu 3D par elles-mêmes sont les consoles dites de 5ème génération. Il y a diverses manières de classer les consoles en générations, la plus commune place la 3D à la 5ème génération, mais détailler ces controverses quant à ce classement nous amènerait trop loin. Les consoles de génération avaient une architecture assez différente des systèmes antérieurs. Les systèmes SGI et assimilés pouvaient se permettre de couter assez cher, d'utiliser beaucoup de circuits, de prendre beaucoup de place. Les bornes d'arcade sont aussi dans ce cas. Aussi, il n'était pas rare que les cartes 3D de l'époque tiennent sur plusieurs cartes électroniques séparées. Mais une console ne peut pas se permettre ce genre de folies. Aussi, les cartes 3D des consoles de l'époque tenaient dans un seul circuit intégré, comme il est d'usage de nos jours. La conséquence est que certains circuits étaient fortement simplifiés, sur les consoles de cinquième génération. Et cela a impacté l'architecture interne des GPU des consoles. Les systèmes SGI avaient plusieurs processeurs pour calculer la géométrie, couplés à plusieurs unités non-programmables pour les pixels/textures. Les cartes 3D des consoles gardaient cette organisation : processeurs pour la géométrie, circuits fixes pour le reste. Mais elles se débrouillaient souvent avec un seul processeur, voire aucun ! Dans ce dernier cas, la géométrie était calculée sur le processeur principal, le CPU. Les unités pour les pixels étaient aussi moins nombreuses, mais il y en avait plusieurs, pour profiter de l'amplification des pixels. : Les cartes 3D des consoles de jeu utilisaient le placage de texture inverse, avec quelques exceptions qui utilisaient le placage de texture direct. ===Le rendu 3D sur les consoles de quatrième génération : la SNES=== Plus haut, j'ai dit que les consoles de quatrième génération n'avaient pas de carte accélératrice 3D. Pourtant, elles ont connus quelques jeux en vraie 3D. La raison à cela est que la 3D était calculée par un GPU placé dans les cartouches du jeu ! Par exemple, les cartouches de Starfox et de Super Mario 2 contenaient un coprocesseur Super FX, qui gérait des calculs de rendu 2D/3D. En tout, il y a environ 16 coprocesseurs pour la SNES et on en trouve facilement la liste sur le net. La console était conçue pour, des pins sur les ports cartouches étaient prévues pour des fonctionnalités de cartouche annexes, dont ces coprocesseurs. Ces pins connectaient le coprocesseur au bus des entrées-sorties. Les coprocesseurs des cartouches de NES avaient souvent de la mémoire rien que pour eux, qui était intégrée dans la cartouche. Ceci étant dit, passons aux consoles de cinquième génération. ===La Nintendo 64 : un GPU avancé=== La Nintendo 64 avait le GPU le plus complexe comparé aux autres consoles, et dépassait même les cartes graphiques des PC. Il faut dire que son GPU a été conçu avec l'aide de l'entreprise SGI, dont on a vu les systèmes graphiques plus haut. Le GPU de la N64 incorporait une unité pour les calculs géométriques, un circuit de rasterisation, une unité de textures et un ROP final pour les calculs de transparence/brouillard/antialiasing, ainsi qu'un circuit pour gérer la profondeur des pixels. En somme, tout le pipeline graphique était implémenté dans le GPU de la Nintendo 64, chose très en avance sur son temps, comparé au PC ou aux autres consoles ! Le GPU est construit autour d'un processeur dédié aux calculs géométriques, le ''Reality Signal Processor'' (RSP), autour duquel on a ajouté des circuits pour le reste du pipeline graphique. L'unité de calcul géométrique est un processeur MIPS R4000, un processeur assez courant à l'époque, auquel on avait retiré quelques fonctionnalités inutiles pour le rendu 3D. Il était couplé à 4 KB de mémoire vidéo, ainsi qu'à 4 KB de mémoire ROM. Le reste du GPU était réalisé avec des circuits fixes. Un point intéressant est que le programme exécuté par le RSP pouvait être programmé ! Le RSP gérait déjà des espèces de proto-shaders, qui étaient appelés des ''[https://ultra64.ca/files/documentation/online-manuals/functions_reference_manual_2.0i/ucode/microcode.html micro-codes]'' dans la documentation de l'époque. La ROM associée au RSP mémorise cinq à sept programmes différents, aux fonctionnalités différentes. * Les microcodes gspFast3D et gspF3DNoN, implémentent un rendu 3D normal, avec des options de ''clipping'' différentes entre les deux. * Le microcode gspTurbo3D fait la même chose, mais avec moins de fonctionnalités et avec une précision réduite. Il ne gère pas le ''clipping'', l'éclairage par pixel, la correction de perspective, l'antialiasing et quelques autres fonctionnalités. Il gère cependant l'éclairage de Gouraud. Il utilise une ''display list'' simplifiée comparé aux deux microcodes précédents. * Le microcode gspZ-Sort effectue une pré-passe z, à savoir qu'il calcule le tampon de profondeur final de la scène 3D, sans rendre l'image. Cela sert à faire une élimination des pixels cachés parfaite, en logiciel. On calcule le tampon de profondeur pour déterminer quels pixels sont visibles, puis une seconde passe rend l'image en, rejetant les pixels non-visibles. * Le microcode gspSprite2D implémente un rendu 2D émulé : les sprites et arrière-plan sont des rectangles texturés. Le microcode gspS2DEX fait la même chose, mais sert à émuler le rendu de la SNES plus qu'autre chose. * Le microcode gspLine3D ne gére que des lignes, pas de triangles. Il sert pour du rendu en fil de fer. Ils géraient le rendu 3D de manière différente et avec une gestion des ressources différentes. Très peu de studios de jeu vidéo ont développé leur propre microcodes N64, car la documentation était mal faite, que Nintendo ne fournissait pas de support officiel pour cela, que les outils de développement ne permettaient pas de faire cela proprement et efficacement. ===La Playstation 1=== Sur la Playstation 1 le calcul de la géométrie était réalisé par le processeur, la carte graphique gérait tout le reste. Et la carte graphique était un circuit fixe spécialisé dans la rasterisation et le placage de textures. Elle utilisait, comme la Nintendo 64, le placage de texture inverse, qui est apparu ensuite sur les cartes graphiques. ===La 3DO et la Sega Saturn=== La Sega Saturn et la 3DO étaient les deux seules consoles à utiliser le rendu direct. La géométrie était calculée sur le processeur, même si les consoles utilisaient parfois un CPU dédié au calcul de la géométrie. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. La Sega Saturn incorpore trois processeurs et deux GPU. Les deux GPUs sont nommés le VDP1 et le VDP2. Le VDP1 s'occupe des textures et des sprites, le VDP2 s'occupe uniquement de l'arrière-plan et incorpore un VDC tout ce qu'il y a de plus simple. Ils ne gèrent pas du tout la géométrie, qui est calculée par les trois processeurs. Le troisième processeur, la Saturn Control Unit, est un processeur de type DSP, à savoir un processeur spécialisé dans le traitement de signal. Il est utilisé presque exclusivement pour accélérer les calculs géométriques. Il avait sa propre mémoire RAM dédiée, 32 KB de SRAM, soit une mémoire locale très rapide. Les transferts entre cette RAM et le reste de l'ordinateur était géré par un contrôleur DMA intégré dans le DSP. En somme, il s'agit d'une sorte de processeur spécialisé dans la géométrie, une sorte d'unité géométrique programmable. Mais la géométrie n'était pas forcément calculée que sur ce DSP, mais pouvait être prise en charge par les 3 CPU. ==L'historique des cartes graphiques des PC, avant l'arrivée de Direct X et Open Gl== Sur PC, l'évolution des cartes graphiques a eu du retard par rapport aux consoles. Les PC sont en effet des machines multi-usage, pour lesquelles le jeu vidéo était un cas d'utilisation parmi tant d'autres. Et les consoles étaient la plateforme principale pour jouer à des jeux vidéo, le jeu vidéo PC étant plus marginal. Mais cela ne veut pas dire que le jeu PC n'existait pas, loin de là ! Un problème pour les jeux PC était que l'écosystème des PC était aussi fragmenté en plusieurs machines différentes : machines Apple 1 et 2, ordinateurs Commdore et Amiga, IBM PC et dérivés, etc. Aussi, programmer des jeux PC n'était pas mince affaire, car les problèmes de compatibilité étaient légion. C'est seulement quand la plateforme x86 des IBM PC s'est démocratisée que l'informatique grand public s'est standardisée, réduisant fortement les problèmes de compatibilité. Mais cela n'a pas suffit, il a aussi fallu que les API 3D naissent. Les API 3D comme Direct X et Open GL sont absolument cruciales pour garantir la compatibilité entre plusieurs ordinateurs aux cartes graphiques différentes. Aussi, l'évolution des cartes graphiques pour PC s'est faite main dans la main avec l'évolution des API 3D. Les fonctionnalités des cartes graphiques ont évolué dans le temps, en suivant les évolutions des API 3D. Du moins dans les grandes lignes, car il est arrivé plusieurs fois que des fonctionnalités naissent sur les cartes graphiques, pour que les fabricants forcent la main de Microsoft ou d'Open GL pour les intégrer de force dans les API 3D. Passons. ===L'introduction des premiers jeux 3D : Quake et les drivers miniGL=== L'API OpenGL est née de la main de SGI, encore eux ! SGI avait créé l'API Iris GL pour ses stations de travail Iris Graphics. Iris GL a ensuite été libéré et est devenu le standard Open GL. Open GL existait déjà avant l'apparition des cartes accélératrices 3D. Il y a avait donc déjà un terreau que les programmeurs graphiques pouvaient utiliser. Mais Open GL était surtout utilisé pour des applications industrielles, médicales (imagerie), graphiques ou militaires, pas pour le jeu vidéo. Mais cela changea avec la sortie du jeu Quake, d'IdSoftware, en 1996. Quake pouvait fonctionner en rendu logiciel, mais le programmeur responsable du moteur 3D (le célébre John Carmack) ajouta une version OpenGL du jeu. Il faut dire que le jeu était programmé sur une station de travail compatible avec OpenGL, même si aucune carte accélératrice de l'époque ne supportait OpenGL. C'était là un choix qui se révéla visionnaire. En théorie, le rendu par OpenGL aurait dû se faire intégralement en logiciel, sauf sur quelques rares stations de travail adaptées. Mais les premières cartes graphiques étaient déjà dans les starting blocks. La toute première carte 3D pour PC est la '''Rendition Vérité V1000''', sortie en Septembre 1995, soit quelques mois avant l'arrivée de la Nintendo 64. La Rendition Vérité V1000 contenait un processeur MIPS cadencé à 25 MHz, 4 mébioctets de RAM, une ROM pour le BIOS, et un RAMDAC, rien de plus. C'était un vrai ordinateur complètement programmable de bout en bout, sans aucun circuit fixe. Les programmeurs ne pouvaient cependant pas utiliser cette programmabilité avec des ''shaders'', mais elle permettait à Rendition d'implémenter n'importe quelle API 3D, que ce soit OpenGL, DirectX ou même sa son API propriétaire. La Rendition Vérité avait de bonnes performances pour ce qui est de la géométrie, mais pas pour le reste. Réaliser la rastérisation et le placage de texture en logiciel n'est pas efficace, pareil pour les opérations de fin de pipeline comme l'antialiasing. Le manque d'unités fixes très rapides pour la rastérisation, le placage de texture ou les opérations de fin de pipeline était clairement un gros défaut. Mais la Rendition Vérité était un cas à part, une exception dans le paysage des cartes 3D de l'époque, qui ne faisait rien comme les autres. Les autres cartes graphiques, sorties peu après, étaient les Voodoo de 3dfx, les Riva TNT de NVIDIA, les Rage/3D d'ATI, la Virge/3D de S3, et la Matrox Mystique. Elles avaient choisit le compromis inverse de la Rendition Vérité V1000 : de bonnes performances pour le placage de textures et la rastérization, mais pas pour les calculs géométriques. Pour rappel, les systèmes professionnels et les consoles avaient des processeurs pour la géométrie, et des circuits fixes pour le reste. Les cartes graphiques de PC se passaient des processeurs pour la géométrie, les calculs géométriques étaient réalisés par le CPU. Les toutes premières cartes 3D pour PC contenaient seulement des circuits pour gérer les textures et des ROPs. Elle géraient le ''z-buffer'' en mémoire vidéo, ainsi que des effets de brouillard. Il n'y avait même pas de circuit pour la rastérisation, qui était faite en logiciel, avec les calculs géométriques. [[File:Architecture de base d'une carte 3D - 2.png|centre|vignette|upright=1.5|Carte 3D sans rasterization matérielle.]] Les cartes suivantes ajoutèrent une gestion des étapes de ''rasterization'' directement en matériel. Les cartes ATI rage 2, les Invention de chez Rendition, et d'autres cartes graphiques supportaient la rasterisation en hardware. [[File:Architecture de base d'une carte 3D - 3.png|centre|vignette|upright=1.5|Carte 3D avec gestion de la géométrie.]] Pour exploiter les unités de texture et le circuit de rastérisation, OpenGL et Direct 3D étaient partiellement implémentées en logiciel, car les cartes graphiques ne supportaient pas toutes les fonctionnalités de l'API. C'était l'époque du miniGL, des implémentations partielles d'OpenGL, fournies par les fabricants de cartes 3D, implémentées dans les pilotes de périphériques de ces dernières. Les fonctionnalités d'OpenGL implémentées dans ces pilotes étaient presque toutes exécutées en matériel, par la carte graphique. Avec l'évolution du matériel, les pilotes de périphériques devinrent de plus en plus complets, au point de devenir des implémentations totales d'OpenGL. Mais au-delà d'OpenGL, chaque fabricant de carte graphique avait sa propre API propriétaire, qui était gérée par leurs pilotes de périphériques (''drivers''). Par exemple, les premières cartes graphiques de 3dfx interactive, les fameuses voodoo, disposaient de leur propre API graphique, l'API Glide. Elle facilitait la gestion de la géométrie et des textures, ce qui collait bien avec l'architecture de ces cartes 3D. Mais ces API propriétaires tombèrent rapidement en désuétude avec l'évolution de DirectX et d'OpenGL. Direct X était une API dans l'ombre d'Open GL. La première version de Direct X qui supportait la 3D était DirectX 2.0 (juin 2, 1996), suivie rapidement par DirectX 3.0 (septembre 1996). Elles dataient d'avant le jeu Quake, et elles étaient très éloignées du hardware des premières cartes graphiques. Elles utilisaient un système d'''execute buffer'' pour communiquer avec la carte graphique, Microsoft espérait que le matériel 3D implémenterait ce genre de système. Ce qui ne fu pas le cas. Direct X 4.0 a été abandonné en cours de développement pour laisser à une version 5.0 assez semblable à la 2.0/3.0. Le mode de rendu laissait de côté les ''execute buffer'' pour coller un peu plus au hardware de l'époque. Mais rien de vraiment probant comparé à Open GL. Même Windows utilisait Open GL au lieu de Direct X maison... C'est avec Direct X 6.0 que Direct X est entré dans la cours des grands. Il gérait la plupart des technologies supportées par les cartes graphiques de l'époque. ===Le ''multi-texturing'' de l'époque Direct X 6.0 : combiner plusieurs textures=== Une technologie très importante standardisée par Dirext X 6 est la technique du '''''multi-texturing'''''. Avec ce qu'on a dit dans le chapitre précédent, vous pensez sans doute qu'il n'y a qu'une seule texture par objet, qui est plaquée sur sa surface. Mais divers effet graphiques demandent d'ajouter des textures par dessus d'autres textures. En général, elles servent pour ajouter des détails, du relief, sur une surface pré-existante. Un exemple intéressant vient des jeux de tir : ajouter des impacts de balles sur les murs. Pour cela, on plaque une texture d'impact de balle sur le mur, à la position du tir. Il s'agit là d'un exemple de '''''decals''''', des petites textures ajoutées sur les murs ou le sol, afin de simuler de la poussière, des impacts de balle, des craquelures, des fissures, des trous, etc. Les textures en question sont de petite taille et se superposent à une texture existante, plus grande. Rendre des ''decals'' demande de pouvoir superposer deux textures. Direct X 6.0 supportait l'application de plusieurs textures directement dans le matériel. La carte graphique devait être capable d'accéder à deux textures en même temps, ou du moins faire semblant que. Pour cela, elle doublaient les unités de texture et adaptaient les connexions entre unités de texture et mémoire vidéo. La mémoire vidéo devait être capable de gérer plusieurs accès mémoire en même temps et devait alors avoir un débit binaire élevé. [[File:Multitexturing.png|centre|vignette|upright=2|Multitexturing]] La carte graphique devait aussi gérer de quoi combiner deux textures entre elles. Par exemple, pour revenir sur l'exemple d'une texture d'impact de balle, il faut que la texture d'impact recouvre totalement la texture du mur. Dans ce cas, la combinaison est simple : la première texture remplace l'ancienne, là où elle est appliquée. Mais les cartes graphiques ont ajouté d'autres combinaisons possibles, par exemple additionner les deux textures entre elle, faire une moyenne des texels, etc. Les opérations pour combiner les textures était le fait de circuits appelés des '''''combiners'''''. Concrètement, les ''combiners'' sont de simples unités de calcul. Les ''conbiners'' ont beaucoup évolués dans le temps, mais les premières implémentation se limitaient à quelques opérations simples : addition, multiplication, superposition, interpolation. L'opération effectuer était envoyée au ''conbiner'' sur une entrée dédiée. [[File:Multitexturing avec combiners.png|centre|vignette|upright=2|Multitexturing avec combiners]] S'il y avait eu un seul ''conbiner'', le circuit de ''multitexturing'' aurait été simplement configurable. Mais dans la réalité, les premières cartes utilisant du ''multi-texturing'' utilisaient plusieurs ''combiners'' placés les uns à la suite des autres. L'implémentation des ''combiners'' retenue par Open Gl, et par le hardware des cartes graphiques, était la suivante. Les ''combiners'' étaient placés en série, l'un à la suite de l'autre, chacun combinant le résultat de l'étage précédent avec une texture. Le premier ''combiner'' gérait l'éclairage par sommet, afin de conserver un minimum de rétrocompatibilité. [[File:Texture combiners Open GL.png|centre|vignette|upright=2|Texture combiners Open GL]] Voici les opérations supportées par les ''combiners'' d'Open GL. Ils prennent en entrée le résultat de l'étage précédent et le combinent avec une texture lue depuis l'unité de texture. {|class="wikitable" |+ Opérations supportées par les ''combiners'' d'Open GL |- ! Replace | colspan="2" | Pixel provenant de l'unité de texture |- ! Addition | colspan="2" | Additionne l'entrée au texel lu. |- ! Modulate | colspan="2" | Multiplie l'entrée avec le texel lu |- ! Mélange (''blending'') | Moyenne pondérée des deux entrées, pondérée par la composante de transparence || La couleur de transparence du texel lu et de l'entrée sont multipliées. |- ! Decals | Moyenne pondérée des deux entrées, pondérée par la composante de transparence. || La transparence du résultat est celle de l'entrée. |} Il faut noter qu'un dernier étage de ''combiners'' s'occupait d'ajouter la couleur spéculaire et les effets de brouillards. Il était à part des autres et n'était pas configurable, c'était un étage fixe, qui était toujours présent, peu importe le nombre de textures utilisé. Il était parfois appelé le '''''combiner'' final''', terme que nous réutiliserons par la suite. Mine de rien, cela a rendu les cartes graphiques partiellement programmables. Le fait qu'il y ait des opérations enchainées à la suite, opérations qu'on peut choisir librement, suffit à créer une sorte de mini-programme qui décide comment mélanger plusieurs textures. Mais il y avait une limitation de taille : le fait que les données soient transmises d'un étage à l'autre, sans détours possibles. Par exemple, le troisième étage ne pouvait avoir comme seule opérande le résultat du second étage, mais ne pouvait pas utiliser celui du premier étage. Il n'y avait pas de registres pour stocker ce qui sortait de la rastérisation, ni pour mémoriser temporairement les texels lus. ===Le ''Transform & Lighting'' matériel de Direct X 7.0=== [[File:Architecture de base d'une carte 3D - 4.png|vignette|upright=1.5|Carte 3D avec gestion de la géométrie.]] La première carte graphique pour PC capable de gérer la géométrie en hardware fût la Geforce 256, la toute première Geforce. Son unité de gestion de la géométrie n'est autre que la bien connue '''unité T&L''' (''Transform And Lighting''). Elle implémentait des algorithmes d'éclairage de la scène 3D assez simples, comme un éclairage de Gouraud, qui étaient directement câblés dans ses circuits. Mais contrairement à la Nintendo 64 et aux bornes d'arcade, elle implémentait le tout, non pas avec un processeur classique, mais avec des circuits fixes. Avec Direct X 7.0 et Open GL 1.0, l'éclairage était en théorie limité à de l'éclairage par sommet, l'éclairage par pixel n'était pas implémentable en hardware. Les cartes graphiques ont tenté d'implémenter l'éclairage par pixel, mais cela n'est pas allé au-delà du support de quelques techniques de ''bump-mapping'' très limitées. Par exemple, Direct X 6.0 implémentait une forme limitée de ''bump-mapping'', guère plus. Un autre problème est qu'il a beaucoup d'algorithmes d'éclairages différents, aux résultats visuels différents, bien au-delà des algorithmes d'éclairage plat, de Gouraud et de Phong. Et les unités de T&L étaient souvent en retard sur les algorithmes logiciels. Les programmeurs avaient le choix entre programmer les algorithmes d’éclairage qu'ils voulaient et les exécuter en logiciel, ou utiliser ceux de l'unité de T&L. Ils choisissaient souvent la première option. Par exemple, Quake 3 Arena et Unreal Tournament n'utilisaient pas les capacités d'éclairage géométrique et préféraient utiliser leurs calculs d'éclairage logiciel fait maison. Cependant, le hardware dépassait les capacités des API et avait déjà commencé à ajouter des capacités de programmation liées au ''multi-texturing''. Les cartes graphiques de l'époque, surtout chez NVIDIA, implémentaient un système de '''''register combiners''''', une forme améliorée de ''texture combiners'', qui permettait de faire une forme limitée d'éclairage par pixel, notamment du vrai ''bump-mampping'', voire du ''normal-mapping''. Mais ce n'était pas totalement supporté par les API 3D de l'époque. Les ''registers combiners'' sont des ''texture combiners'' mais dans lesquels ont aurait retiré la stricte organisation en série. Il y a toujours plusieurs étages à la suite, qui peuvent exécuter chacun une opération, mais tous les étages ont maintenant accès à toutes les textures lues et à tout ce qui sort de la rastérisation, pas seulement au résultat de l'étape précédente. Pour cela, on ajoute des registres pour mémoriser ce qui sort des unités de texture, et pour ce qui sort de la rastérisation. De plus, on ajoute des registres temporaires pour mémoriser les résultats de chaque ''combiner'', de chaque étage. Il faut cependant signaler qu'il existe un ''combiner'' final, séparé des étages qui effectuent des opérations proprement dits. Il s'agit de l'étage qui applique la couleur spéculaire et les effets de brouillards. Il ne peut être utilisé qu'à la toute fin du traitement, en tant que dernier étage, on ne peut pas mettre d'opérations après lui. Sa sortie est directement connectée aux ROPs, pas à des registres. Il faut donc faire la distinction entre les '''''combiners'' généraux''' qui effectuent une opération et mémorisent le résultat dans des registres, et le ''combiner'' final qui envoie le résultat aux ROPs. L'implémentation des ''register combiners'' utilisait un processeur spécialisés dans les traitements sur des pixels, une sorte de proto-processeur de ''shader''. Le processeur supportait des opérations assez complexes : multiplication, produit scalaire, additions. Il s'agissait d'un processeur de type VLIW, qui sera décrit dans quelques chapitres. Mais ce processeur avait des programmes très courts. Les premières cartes NVIDIA, comme les cartes TNT pouvaient exécuter deux opérations à la suite, suivie par l'application de la couleurs spéculaire et du brouillard. En somme, elles étaient limitées à un ''shader'' à deux/trois opérations, mais c'était un début. Le nombre d'opérations consécutives est rapidement passé à 8 sur la Geforce 3. ===L'arrivée des ''shaders'' avec Direct X 8.0=== [[File:Architecture de la Geforce 3.png|vignette|upright=1.5|Architecture de la Geforce 3]] Les ''register combiners'' était un premier pas vers un éclairage programmable. Paradoxalement, l'évolution suivante s'est faite non pas dans l'unité de rastérisation/texture, mais dans l'unité de traitement de la géométrie. La Geforce 3 a remplacé l'unité de T&L par un processeur capable d'exécuter des programmes. Les programmes en question complétaient l'unité de T&L, afin de pouvoir rajouter des techniques d'éclairage plus complexes. Le tout a permis aussi d'ajouter des animations, des effets de fourrures, des ombres par ''shadow volume'', des systèmes de particule évolués, et bien d'autres. À partir de la Geforce 3 de Nvidia, les cartes graphiques sont devenues capables d'exécuter des programmes appelés '''''shaders'''''. Le terme ''shader'' vient de ''shading'' : ombrage en anglais. Grace aux ''shaders'', l'éclairage est devenu programmable, il n'est plus géré par des unités d'éclairage fixes mais été laissé à la créativité des programmeurs. Les programmeurs ne sont plus vraiment limités par les algorithmes d'éclairage implémentés dans les cartes graphiques, mais peuvent implémenter les algorithmes d'éclairage qu'ils veulent et peuvent le faire exécuter directement sur la carte graphique. Les ''shaders'' sont classifiés suivant les données qu'ils manipulent : '''''pixel shader''''' pour ceux qui manipulent des pixels, '''''vertex shaders''''' pour ceux qui manipulent des sommets. Les premiers sont utilisés pour implémenter l'éclairage par pixel, les autres pour gérer tout ce qui a trait à la géométrie, pas seulement l'éclairage par sommets. Direct X 8.0 avait un standard pour les shaders, appelé ''shaders 1.0'', qui correspondait parfaitement à ce dont était capable la Geforce 3. Il standardisait les ''vertex shaders'' de la Geforce 3, mais il a aussi renommé les ''register combiners'' comme étant des ''pixel shaders'' version 1.0. Les ''register combiners'' n'ont pas évolués depuis la Geforce 256, si ce n'est que les programmes sont passés de deux opérations successives à 8, et qu'il y avait possibilité de lire 4 textures en ''multitexturing''. A l'opposé, le processeur de ''vertex shader'' de la Geforce 3 était capable d'exécuter des programmes de 128 opérations consécutives et avait 258 registres différents ! Des ''pixels shaders'' plus évolués sont arrivés avec l'ATI Radeon 8500 et ses dérivés. Elle incorporait la technologie ''SMARTSHADER'' qui remplacait les ''registers combiners'' par un processeur de ''shader'' un peu limité. Un point est que le processeur acceptait de calculer des adresses de texture dans le ''pixel shader''. Avant, les adresses des texels à lire étaient fournis par l'unité de rastérisation et basta. L'avantage est que certains effets graphiques étaient devenus possibles : du ''bump-mapping'' avancé, des textures procédurales, de l'éclairage par pixel anisotrope, du éclairage de Phong réel, etc. Avec la Radeon 8500, le ''pixel shader'' pouvait calculer des adresses, et lire les texels associés à ces adresses calculées. Les ''pixel shaders'' pouvaient lire 6 textures, faire 8 opérations sur les texels lus, puis lire 6 textures avec les adresses calculées à l'étape précédente, et refaire 8 opérations. Quelque chose de limité, donc, mais déjà plus pratique. Les ''pixel shaders'' de ce type ont été standardisé dans Direct X 8.1, sous le nom de ''pixel shaders 1.4''. Encore une fois, le hardware a forcé l'intégration dans une API 3D. ===Les ''shaders'' de Direct X 9.0 : de vrais ''pixel shaders''=== Avec Direct X 9.0, les ''shaders'' sont devenus de vrais programmes, sans les limitations des ''shaders'' précédents. Les ''pixels shaders'' sont passés à la version 2.0, idem pour les ''vertex shaders''. Concrètement, ils ont des fonctionnalités bien supérieures à celles des ''registers combiners''. Les ''shaders'' pouvaient exécuter une suite d'opérations arbitraire, dans le sens où elle n'était pas structurée avec tel type d'opération au début, suivie par un accès aux textures, etc. On pouvait mettre n'importe quelle opération dans n'importe quel ordre. De plus, les ''shaders'' ne sont plus écrit en assembleur comme c'était le cas avant. Ils sont dorénavant écrits dans un langage de haut-niveau, le HLSL pour les shaders Direct X et le GLSL pour les shaders Open Gl. Les ''shaders'' sont ensuite traduit (compilés) en instructions machines compréhensibles par la carte graphique. Au début, ces langages et la carte graphique supportaient uniquement des opérations simples. Mais au fil du temps, les spécifications de ces langages sont devenues de plus en plus riches à chaque version de Direct X ou d'Open Gl, et le matériel en a fait autant. Le matériel s'est alors adapté, en incorporant un véritable processeur pour les ''pixel shaders''. Les ''pixel shaders'' sont maintenant exécutés par un processeur de ''shader'' dédié, aux fonctionnalités bien supérieures à celles des ''registers combiners''. Le processeur de ''pixel shader'' incorpore l'unité de texture en sont sein, les deux sont fusionnés. La raison à cela sera expliqué dans la suite du chapitre. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=1.5|Carte 3D avec pixels et vertex shaders non-unifiés.]] ===L'après Direct X 9.0=== Avant Direct X 10, les processeurs de ''shaders'' ne géraient pas exactement les mêmes opérations pour les processeurs de ''vertex shader'' et de ''pixel shader''. Les processeurs de ''vertex shader'' et de ''pixel shader''étaient séparés. Depuis DirectX 10, ce n'est plus le cas : le jeu d'instructions a été unifié entre les vertex shaders et les pixels shaders, ce qui fait qu'il n'y a plus de distinction entre processeurs de vertex shaders et de pixels shaders, chaque processeur pouvant traiter indifféremment l'un ou l'autre. [[File:Architecture de base d'une carte 3D - 6.png|centre|vignette|upright=1.5|Architecture de la GeForce 6800.]] Avec Direct X 10, de nombreux autres ''shaders'' sont apparus. Les plus liés au rendu 3D sont les '''''geometry shader''''' pour ceux qui manipulent des triangles, de ''hull shaders'' et de ''domain shaders'' pour la tesselation. De plus, les cartes graphiques modernes sont capables d’exécuter des programmes informatiques qui n'ont aucun lien avec le rendu 3D, mais sont exécutés par la carte graphique comme le ferait un processeur d'ordinateur normal. De tels ''shaders'' sans lien avec le rendu 3D sont appelés des ''compute shader''. ==Les cartes graphiques d'aujourd'hui== Les circuits d'un GPU ont beaucoup évolué depuis l'introduction des ''shaders'', pour devenir de plus en plus programmables. Mais à côté des processeurs de ''shaders'', il reste quelques circuits non-programmables appelés des circuits fixes. La rastérisation, le placage de texture, l'élimination des pixels cachés et le mélange ''alpha'' sont gérés par des circuits fixes. [[File:3D-Pipeline.svg|centre|vignette|upright=3.0|Pipeline 3D : ce qui est programmable et ce qui ne l'est pas dans une carte graphique moderne.]] Mais pourquoi ne pas tout rendre programmable ? Ou au contraire, utiliser seulement des circuits fixes ? La réponse rapide est qu'il s'agit d'un compromis entre flexibilité et performance qui permet d'avoir le meilleur des deux mondes. Mais ce compromis a fortement évolué dans le temps, comme on va le voir plus bas. Rendre l'éclairage programmable permet d'implémenter facilement un grand nombre d'effets graphiques sans avoir à les implémenter en hardware. Avant les ''shaders'', les effets graphiques derniers cri n'étaient disponibles que sur les derniers modèles de carte graphique. Avec des ''vertex/pixel shaders'', ce genre de défaut est passé à la trappe. Si un nouvel algorithme de rendu graphique est inventé, il peut être utilisé dès le lendemain sur toutes les cartes graphiques modernes. De plus, implémenter beaucoup d'algorithmes d'éclairage différents avec des circuits fixes a un cout en termes de transistors, alors qu'utiliser des circuits programmable a un cout en hardware plus limité. Tout cela est à l'exact opposé de ce qu'on a avec les autres circuits, comme les circuits pour la rastérisation ou le placage de texture. Il n'y a pas 36 façons de rastériser une scène 3D et la flexibilité n'est pas un besoin important pour cette opération, alors que les performances sont cruciales. Même chose pour le placage/filtrage de textures. En conséquences, les unités de rastérisation, de texture, et les ROPs sont toutes implémentées en matériel. Faire ainsi permet de gagner en performance sans que cela ait le moindre impact pour le programmeur. Reste à expliquer dans le détail pourquoi. ===Les unités de texture sont intégrées aux processeurs de shaders=== Avec l'arrivée des processeurs de shaders, les unités de texture ont été intégrées dans les processeurs de shaders eux-mêmes. C'est la seule unité fixe qui a subit ce traitement, et il est intéressant de comprendre pourquoi. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=2|Architecture de base d'une carte 3D.]] Pour cela, il faut faire un rappel sur ce qu'il y a dans un processeur. Un processeur contient globalement quatre circuits : * une unité de calcul qui fait des calculs ; * des registres pour stocker les opérandes et résultats des calculs ; * une unité de communication avec la mémoire ; * et un séquenceur, un circuit de contrôle qui commande les autres. L'unité de communication avec la mémoire sert à lire ou écrire des données, à les transférer de la RAM vers les registres, ou l'inverse. Lire une donnée demande d'envoyer son adresse à la RAM, qui répond en envoyant la donnée lue. Elle est donc toute indiquée pour lire une texture : lire une texture n'est qu'un cas particulier de lecture de données. Les texels à lire sont à une adresse précise, la RAM répond à la lecture avec le texel demandé. Il est donc possible d'utiliser l'unité de communication avec la mémoire comme si c'était une unité de texture. Cependant, les textures ne sont pas utilisées comme telles de nos jours. Le rendu 3D moderne utilise des techniques dites de filtrage de texture, qui permettent d'améliorer la qualité du rendu des textures. Sans ce filtrage de texture, les textures appliquées naïvement donnent un résultat assez pixelisé et assez moche, pour des raisons assez techniques. Le filtrage élimine ces artefacts, en utilisant une forme d'''antialiasing'' interne aux textures, le fameux filtrage de texture. Le filtrage de texture peut être réalisé en logiciel ou en matériel. Techniquement, il est possible de le faire dans un ''shader''. Le ''shader'' calcule les adresses des texels à lire, lit les texels, et effectue ensuite le filtrage avec des opérations de calcul. Mais ce n'est pas ce qui est fait, le filtrage de texture est toujours effectué directement en matériel. La raison est que le filtrage de texture est très simple à implémenter en hardware. Le filtrage bilinéaire ou trilinéaire demande juste des circuits d'interpolation et quelques registres, ce qui est trivial. Et la seconde raison est qu'il n'y a pas 36 façons de filtrer des textures : une carte graphique peut implémenter les algorithmes principaux existants en assez peu de circuits. Pour simplifier l'implémentation, les processeurs de ''shader'' modernes disposent d'une unité d'accès mémoire séparée de l'unité de texture. L'unité d'accès mémoire normale s'occupe des accès mémoire hors-textures, alors que l'unité mémoire s'occupe de lire les textures. L'unité de texture contient de quoi faire du filtrage de texture, mais aussi faire des calculs d'adresse spécialisées, intrinsèquement liés au format des textures, qu'on détaillera dans le chapitre sur les textures. En comparaison, les unités d'accès mémoire effectuent des calculs d'adresse plus basiques. Un dernier avantage est que l'unité de texture est reliée au cache de texture, alors que l'unité d'accès mémoire est relié au cache L1/L2. : Il faut noter que les ROPs peuvent aussi être intégré dans les processeurs de ''shader'', mais c'est assez rare. Les cartes graphiques pour PC ont des ROPs séparés des processeurs de ''shaders''. Par contre, quelques cartes graphiques destinées les smartphones et autres appareils mobiles font exception. Sur celles-ci, les unités de textures et les ROPs sont intégrés dans les processeurs de ''shaders'', aux côtés de l'unité d'accès mémoire LOAD/STORE. Il s'agit de cartes graphiques en rendu à tuiles. {{NavChapitre | book=Les cartes graphiques | prev=Les cartes graphiques : architecture de base | prevText=Les cartes graphiques : architecture de base | next=Les processeurs de shaders | nextText=Les processeurs de shaders }} {{autocat}} e56fa9hyd9fr2z0sakj8ubxdlr640fe 763503 763502 2026-04-11T19:30:47Z Mewtow 31375 /* Les unités de texture sont intégrées aux processeurs de shaders */ 763503 wikitext text/x-wiki Il est intéressant d'étudier le hardware des cartes graphiques en faisant un petit résumé de leur évolution dans le temps. En effet, leur hardware a fortement évolué dans le temps. Et il serait difficile à comprendre le hardware actuel sans parler du hardware d'antan. En effet, une carte graphique moderne est partiellement programmable. Certains circuits sont totalement programmables, d'autres non. Et pour comprendre pourquoi, il faut étudier comment ces circuits ont évolués. Le hardware des cartes graphiques a fortement évolué dans le temps, ce qui n'est pas une surprise. Les évolutions de la technologie, avec la miniaturisation des transistors et l'augmentation de leurs performances a permis aux cartes graphiques d'incorporer de plus en plus de circuits avec les années. Avant l'invention des cartes graphiques, toutes les étapes du pipeline graphique étaient réalisées par le processeur : il calculait l'image à afficher, et l’envoyait à une carte d'affichage 2D. Au fil du temps, de nombreux circuits furent ajoutés, afin de déporter un maximum de calculs vers la carte vidéo. Le rendu 3D moderne est basé sur le placage de texture inverse, avec des coordonnées de texture, une correction de perspective, etc. Mais les anciennes consoles et bornes d'arcade utilisaient le placage de texture direct. Et cela a impacté le hardware des consoles/PCs de l'époque. Avec le placage de texture direct, il était primordial de calculer la géométrie, mais la rasterisation était le fait de VDC améliorés. Aussi, les premières bornes d'arcade 3D et les consoles de 5ème génération disposaient processeurs pour calculer la géométrie et de circuits d'application de textures très particuliers. A l'inverse, les PC utilisaient un rendu inverse, totalement différent. Sur les PC, les premières cartes graphiques avaient un circuit de rastérisation et des unités de textures, mais pas de circuits géométriques. ==Les premières cartes graphiques== Dès les années 70-80, le rendu 3D était utilisé par de nombreuses entreprises industrielles : des applications de visualisation 3D étaient utilisées en architecture, des applications de conception assistée par ordinateur étaient déjà d'utilisation courante, sans compter les simulateurs de vol utilisés par l'armée et les instructeurs qui formaient les pilotes d'avion. Le rendu 3D était aussi étudié au niveau académique, la recherche en 3D était déjà florissante. Il existait même du matériel spécifiquement conçu pour le rendu graphique, mais celui-ci était spécifiquement dédié à des super-calculateurs ou des ''workstations'' (des sortes d'ancêtres des PC, très puissants pour l'époque, mais conçus uniquement pour les entreprises). ===Le début des années 80 : le rendu en fils de fer=== Le tout premier système de ce genre était le '''''Line Drawing System-1''''' de l'entreprise Evans & Sutherland, daté de 1969. Ce n'est ni plus ni moins que le toute premier circuit graphique séparé du processeur ayant existé. C'est en un sens la toute première carte graphique, le tout premier GPU. Il prenait la forme d'un périphérique qui se connectait à l'ordinateur d'un côté et était relié à l'écran de l'autre. Il était compatible avec un grand nombre d'ordinateurs et de processeurs existants. Il a été suivi par plusieurs successeurs, nommés ''Picture System 1, 2'' et le ''PS300 series''. [[File:Evans & Sutherland LDS-1 (1).jpg|vignette|Evans & Sutherland LDS-1 (1)]] Ils permettaient de faire du rendu en fil de fer, sans texture ni même sans polygones colorés. Un tel rendu était utile pour des applications assez limitées : architecture, dessin de molécules pour les entreprises pharmaceutique et certains centres de recherche, l'aérospatiale, etc. Ces cartes graphiques étaient utilisées de concert avec des écrans appelés '''écrans vectoriels''' (''vector display''). Pour simplifier, ils ressemblaient à des écrans CRT, sauf que le faisceau d'électron ne balayait pas l'écran ligne par ligne, mais traçait des lignes arbitraires à l'écran. On lui précisait deux points de coordonnées x1,y1 ; et x2,y2 ; puis l'écran tracait une ligne entre ces deux points. En général, la ligne tracée était maintenue pendant un long moment, entre plusieurs secondes et plusieurs minutes. L'intérieur du circuit était assez simple : un circuit de multiplication de matrice pour les calculs géométriques, un rastériser simplifié (le ''clipping diviser''), un circuit de tracé de lignes, et un processeur de contrôle pour commander les autres circuits. Le fait que ces trois circuits soient séparés permettait une implémentation en pipeline, où plusieurs portions de l'image pouvaient être calculées en même temps : pendant que l'une est dans l'unité géométrique, l'autre est dans le rastériseur et une troisième est en cours de tracé. [[File:Lds1blockdiagram05.svg|centre|vignette|upright=2|Architecture du LDS-1. Le processeur de contrôle n'est pas représenté.]] Le processeur de contrôle exécute un programme qui se charge de commander l'unité géométrique et les autres circuits. Le programme en question est fourni par le programmeur, le LDS-1 est donc totalement programmable. Il lit directement les données nécessaires pour le rendu dans la mémoire de l’ordinateur et le programme exécuté est lui aussi en mémoire principale. Il n'a pas de mémoire vidéo dédiée, il utilise la RAM de l'ordinateur principal. Le multiplieur de matrices est plus complexe qu'on pourrait s'y attendre. Il ne s'agit pas que d'un circuit arithmétique tout simple, mais d'un véritable processeur avec des registres et des instructions machine complexes. Il contient plusieurs registres, l'ensemble mémorisant 4 matrices de 16 nombres chacune (4 lignes de 4 colonnes). Un nombre est codé sur 18 bits. Les registres sont reliés à un ensemble de circuits arithmétiques, des additionneurs et des multiplieurs. Le circuit supporte des instructions de copie entre registres, pour copier une ligne d'une matrice à une autre, des instructions LOAD/STORE pour lire ou écrire dans la mémoire RAM, etc. Il supporte aussi des multiplications en 2D et 3D. Le ''clipping divider'' est un circuit assez complexe, contenant un processeur à accumulateur, une mémoire ROM pour le programme du processeur. Le programme exécuté par le processeur est un petit programme de 62 instructions, stocké dans la ROM. L'algorithme du ''clipping divider'' est décrite dans le papier de recherche "A clipping divider", écrit par Robert Sproull. Un détail assez intéressant est que le résultat en sortie de l'unité géométrique et du rastériseur peuvent être envoyés à l'ordinateur en parallèle du rendu. C'était très utile sur les anciens ordinateurs qui étaient connectés à plusieurs terminaux. Le LDS-1 calculait la géométrie et le rendu, et le tout pouvait petre envoyé à d'autres composants, comme des terminaux, une imprimante, etc. ===Les systèmes ultérieurs : rendu à triangles colorés et texturé=== Les systèmes précédents étaient très limités : ils calculaient la géométrie et n'avaient pas de ''framebuffer'', ni de tampon de profondeur, ni gestion de l'éclairage, ni quoique ce soit. De tels systèmes étaient donc des accélérateurs géométriques que de vrais systèmes graphiques complets, du fait de l'absence de ''framebuffer''. Ils étaient composés de processeurs spécialisés dans les calculs à virgule flottante, faisant des calculs géométriques, et éventuellement d'un processeur pour la rastérisation. La raison est que la RAM était très chère et que créer des circuits fixes étaient très chers et peu disponibles. Par contre, les processeurs à virgule flottante étaient peu chers et facile à trouver. Vers la fin des années 80, grâce à la baisse du prix de la RAM et la démocratisation des ASIC (des circuits fixes fait sur mesure), ajouter un ''framebuffer'' est est devenu possible. C'est alors que sont apparus les '''systèmes de rendu 3D de première génération'''. De tels systèmes ont permis d'implémenter le rendu à primitives colorées qu'on a vu il y a quelques chapitres, à savoir un rendu où les triangles sont coloriés avec une couleur unique. Les systèmes de première génération étaient simples : des processeurs pour le calcul de la géométrie, un circuit de rastérisation, une RAM pour le ''framebuffer'' et des ASIC servant de ROPs très simples. Il n'y avait pas d'élimination des pixels cachés, pas de textures, et encore moins d'éclairage par pixels. Le premier système de ce genre était le ''Shaded Picture System'', toujours par Evans & Sutherland. Il ne gérait pas la couleur et ne pouvait afficher que des images en noir et blanc, mais il gérait l'éclairage par sommet (''vertex lighting''). Il a rapidement été dépassé par les systèmes de l'entreprise ''Silicon Graphics Inc'' (SGI), ainsi que ceux de l'entreprise Apollo avec sa série Apollo DN. Les '''systèmes de seconde génération''' sont apparus vers la fin des années 80, et se distinguent des précédents par l'ajout un tampon de profondeur. Ils intègrent aussi des capacités d'éclairage par pixel, à savoir de l'éclairage plat, de Gouraud, voire de Phong ! Enfin, les '''systèmes de troisième génération''' ont acquis des capacités de placage de texture, que les systèmes précédents n'avaient pas. Ils ont aussi ajouté un support de l'antialiasing. Les systèmes SGI avec placage de texture ont déjà été abordé au chapitre précédent, dans la section sur les GPU en mode immédiat et à ''tile''. Aussi, nous ne reviendrons pas dessus. [[File:Evolution de l'architecture des premières cartes graphiques, dans les années 80-90.png|centre|vignette|upright=2.5|Evolution de l'architecture des premières cartes graphiques, dans les années 80-90]] Les systèmes de première, seconde et troisième génération avaient de nombreux points communs. En premier lieu, ils étaient fabriqués en connectant plusieurs cartes électroniques : une carte pour les calculs géométriques, une ou plusieurs cartes pour le reste du rendu graphique, une carte dédiée au VDC et avec un connecteur écran. Les transistors de l'époque n'étaient pas encore miniaturisés, ce qui fait que le système graphique ne pouvait pas tenir sur une seule carte électronique. Il n'y avait donc pas de carte graphique proprement dit, mais un équivalent éclaté sur plusieurs cartes électroniques. La carte pour la géométrie contenait typiquement une mémoire FIFO pour accumuler les commandes de rendu, un processeur de commande, et plusieurs processeurs géométriques. Les processeurs géométriques étaient parfois conçus sur mesure, comme l'a été le le ''Geometry Engine'' de SGI. Mais il est arrivé qu'ils utilisent des processeurs commerciaux comme le Weitek 3222, l'Intel i860, etc. Les processeurs pouvaient être placés en série ou en parallèle, comme expliqué dans le chapitre précédent. Le circuit de rastérisation était réalisé soit avec un processeur dédié, soit avec un circuit fixe, soit un mélange des deux. La rastérisation est en effet réalisée en plusieurs étapes, certaines peuvent être implémentées avec un processeur et d'autres avec des circuits fixes. Un point important est qu'à l'époque, le rendu n'utilisait pas que des triangles, mais des polygones en général. Ce n'est que par la suite que le rendu s'est focalisé sur les triangles et les ''quads'' (quadrilatères). Il arrivait que le système graphique gérait partiellement des polygones concaves, voire convexes. Sur les systèmes SGI, les calculs géométriques se faisaient avec des polygones, que la rastérisation découpait en triangles, le reste du rendu se faisait avec des triangles. Les stations de travail Apollo DN 10000VS découpaient les polygones en trapézoïdes orientés à l'horizontale, alignés avec des ''scanlines''. D'autres systèmes découpaient tout en triangle lors de l'étape géométrique ==Les précurseurs grand public : les bornes d'arcade== [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] L'accélération du rendu 3D sur les bornes d'arcade était déjà bien avancé dès les années 90. Les bornes d'arcade ont toujours été un segment haut de gamme de l'industrie du jeu vidéo, aussi ce n'est pas étonnant. Le prix d'une borne d'arcade dépassait facilement les 10 000 dollars pour les plus chères et une bonne partie du prix était celui du matériel informatique. Le matériel était donc très puissant et débordait de mémoire RAM comparé aux consoles de jeu et aux PC. La plupart des bornes d'arcade utilisaient du matériel standardisé entre plusieurs bornes. A l'intérieur d'une borne d'arcade se trouve une '''carte de borne d'arcade''' qui est une carte mère avec un ou plusieurs processeurs, de la RAM, une carte graphique, un VDC et pas mal d'autres matériels. La carte est reliée aux périphériques de la borne : joysticks, écran, pédales, le dispositif pour insérer les pièces afin de payer, le système sonore, etc. Le jeu utilisé pour la borne est placé dans une cartouche qui est insérée dans un connecteur spécialisé. Les cartes de bornes d'arcade étaient généralement assez complexes, elles avaient une grande taille et avaient plus de composants que les cartes mères de PC. Chaque carte contenait un grand nombre de chips pour la mémoire RAM et ROM, et il n'était pas rare d'avoir plusieurs processeurs sur une même carte. Et il n'était pas rare d'avoir trois à quatre cartes superposées dans une seule borne. Pour ceux qui veulent en savoir plus, Fabien Sanglard a publié gratuitement un livre sur le fonctionnement des cartes d'arcade CPS System, disponible via ce lien : [https://fabiensanglard.net/b/cpsb.pdf The book of CP System]. Les premières cartes graphiques des bornes d'arcade étaient des cartes graphiques 2D auxquelles on avait ajouté quelques fonctionnalités. Les sprites pouvaient être tournés, agrandit/réduits, ou déformés pour simuler de la perspective et faire de la fausse 3D. Par la suite, le vrai rendu 3D est apparu sur les bornes d'arcade. Dès 1988, la carte d'arcade Namco System 21 et Sega Model 1 géraient les calculs géométriques. Quelques années plus tard, les cartes graphiques se sont mises à supporter un éclairage de Gouraud et du placage de texture. Par exemple, le Namco System 22 et la Sega model 2 supportaient des textures 2D et comme le filtrage de texture (bilinéaire et trilinéaire), le mip-mapping, et quelques autres. Au passage, les cartes graphiques de la Namco System 22 étaient développées en partenariat avec Eans & Sutherland, qui avait commencé à se diversifier dans le marché grand public. Les cartes graphiques de l'époque faisaient les calculs géométriques sur plusieurs processeurs, généralement des processeurs de type DSP (des processeurs spécialisés dans le traitement de signal). Par exemple, la Namco System 2 utilisait 4 DSP de marque Texas Instruments TMS320C25, cadencés à 24,576 MHz. La carte d'arcade Sega Model 1 utilisait quant à elle un DSP spécialisé dans les calculs géométriques. Par la suite, les bornes d'arcade ont réutilisé le hardware des PC et autres consoles de jeux. ==La 3D sur les consoles de quatrième/cinquième génération== Les consoles avant la quatrième génération de console étaient des consoles purement 2D, sans circuits d'accélération 3D. Leur carte graphique était un simple VDC 2D, plus ou moins performant selon la console. Les premières consoles de jeu capables de rendu 3D par elles-mêmes sont les consoles dites de 5ème génération. Il y a diverses manières de classer les consoles en générations, la plus commune place la 3D à la 5ème génération, mais détailler ces controverses quant à ce classement nous amènerait trop loin. Les consoles de génération avaient une architecture assez différente des systèmes antérieurs. Les systèmes SGI et assimilés pouvaient se permettre de couter assez cher, d'utiliser beaucoup de circuits, de prendre beaucoup de place. Les bornes d'arcade sont aussi dans ce cas. Aussi, il n'était pas rare que les cartes 3D de l'époque tiennent sur plusieurs cartes électroniques séparées. Mais une console ne peut pas se permettre ce genre de folies. Aussi, les cartes 3D des consoles de l'époque tenaient dans un seul circuit intégré, comme il est d'usage de nos jours. La conséquence est que certains circuits étaient fortement simplifiés, sur les consoles de cinquième génération. Et cela a impacté l'architecture interne des GPU des consoles. Les systèmes SGI avaient plusieurs processeurs pour calculer la géométrie, couplés à plusieurs unités non-programmables pour les pixels/textures. Les cartes 3D des consoles gardaient cette organisation : processeurs pour la géométrie, circuits fixes pour le reste. Mais elles se débrouillaient souvent avec un seul processeur, voire aucun ! Dans ce dernier cas, la géométrie était calculée sur le processeur principal, le CPU. Les unités pour les pixels étaient aussi moins nombreuses, mais il y en avait plusieurs, pour profiter de l'amplification des pixels. : Les cartes 3D des consoles de jeu utilisaient le placage de texture inverse, avec quelques exceptions qui utilisaient le placage de texture direct. ===Le rendu 3D sur les consoles de quatrième génération : la SNES=== Plus haut, j'ai dit que les consoles de quatrième génération n'avaient pas de carte accélératrice 3D. Pourtant, elles ont connus quelques jeux en vraie 3D. La raison à cela est que la 3D était calculée par un GPU placé dans les cartouches du jeu ! Par exemple, les cartouches de Starfox et de Super Mario 2 contenaient un coprocesseur Super FX, qui gérait des calculs de rendu 2D/3D. En tout, il y a environ 16 coprocesseurs pour la SNES et on en trouve facilement la liste sur le net. La console était conçue pour, des pins sur les ports cartouches étaient prévues pour des fonctionnalités de cartouche annexes, dont ces coprocesseurs. Ces pins connectaient le coprocesseur au bus des entrées-sorties. Les coprocesseurs des cartouches de NES avaient souvent de la mémoire rien que pour eux, qui était intégrée dans la cartouche. Ceci étant dit, passons aux consoles de cinquième génération. ===La Nintendo 64 : un GPU avancé=== La Nintendo 64 avait le GPU le plus complexe comparé aux autres consoles, et dépassait même les cartes graphiques des PC. Il faut dire que son GPU a été conçu avec l'aide de l'entreprise SGI, dont on a vu les systèmes graphiques plus haut. Le GPU de la N64 incorporait une unité pour les calculs géométriques, un circuit de rasterisation, une unité de textures et un ROP final pour les calculs de transparence/brouillard/antialiasing, ainsi qu'un circuit pour gérer la profondeur des pixels. En somme, tout le pipeline graphique était implémenté dans le GPU de la Nintendo 64, chose très en avance sur son temps, comparé au PC ou aux autres consoles ! Le GPU est construit autour d'un processeur dédié aux calculs géométriques, le ''Reality Signal Processor'' (RSP), autour duquel on a ajouté des circuits pour le reste du pipeline graphique. L'unité de calcul géométrique est un processeur MIPS R4000, un processeur assez courant à l'époque, auquel on avait retiré quelques fonctionnalités inutiles pour le rendu 3D. Il était couplé à 4 KB de mémoire vidéo, ainsi qu'à 4 KB de mémoire ROM. Le reste du GPU était réalisé avec des circuits fixes. Un point intéressant est que le programme exécuté par le RSP pouvait être programmé ! Le RSP gérait déjà des espèces de proto-shaders, qui étaient appelés des ''[https://ultra64.ca/files/documentation/online-manuals/functions_reference_manual_2.0i/ucode/microcode.html micro-codes]'' dans la documentation de l'époque. La ROM associée au RSP mémorise cinq à sept programmes différents, aux fonctionnalités différentes. * Les microcodes gspFast3D et gspF3DNoN, implémentent un rendu 3D normal, avec des options de ''clipping'' différentes entre les deux. * Le microcode gspTurbo3D fait la même chose, mais avec moins de fonctionnalités et avec une précision réduite. Il ne gère pas le ''clipping'', l'éclairage par pixel, la correction de perspective, l'antialiasing et quelques autres fonctionnalités. Il gère cependant l'éclairage de Gouraud. Il utilise une ''display list'' simplifiée comparé aux deux microcodes précédents. * Le microcode gspZ-Sort effectue une pré-passe z, à savoir qu'il calcule le tampon de profondeur final de la scène 3D, sans rendre l'image. Cela sert à faire une élimination des pixels cachés parfaite, en logiciel. On calcule le tampon de profondeur pour déterminer quels pixels sont visibles, puis une seconde passe rend l'image en, rejetant les pixels non-visibles. * Le microcode gspSprite2D implémente un rendu 2D émulé : les sprites et arrière-plan sont des rectangles texturés. Le microcode gspS2DEX fait la même chose, mais sert à émuler le rendu de la SNES plus qu'autre chose. * Le microcode gspLine3D ne gére que des lignes, pas de triangles. Il sert pour du rendu en fil de fer. Ils géraient le rendu 3D de manière différente et avec une gestion des ressources différentes. Très peu de studios de jeu vidéo ont développé leur propre microcodes N64, car la documentation était mal faite, que Nintendo ne fournissait pas de support officiel pour cela, que les outils de développement ne permettaient pas de faire cela proprement et efficacement. ===La Playstation 1=== Sur la Playstation 1 le calcul de la géométrie était réalisé par le processeur, la carte graphique gérait tout le reste. Et la carte graphique était un circuit fixe spécialisé dans la rasterisation et le placage de textures. Elle utilisait, comme la Nintendo 64, le placage de texture inverse, qui est apparu ensuite sur les cartes graphiques. ===La 3DO et la Sega Saturn=== La Sega Saturn et la 3DO étaient les deux seules consoles à utiliser le rendu direct. La géométrie était calculée sur le processeur, même si les consoles utilisaient parfois un CPU dédié au calcul de la géométrie. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. La Sega Saturn incorpore trois processeurs et deux GPU. Les deux GPUs sont nommés le VDP1 et le VDP2. Le VDP1 s'occupe des textures et des sprites, le VDP2 s'occupe uniquement de l'arrière-plan et incorpore un VDC tout ce qu'il y a de plus simple. Ils ne gèrent pas du tout la géométrie, qui est calculée par les trois processeurs. Le troisième processeur, la Saturn Control Unit, est un processeur de type DSP, à savoir un processeur spécialisé dans le traitement de signal. Il est utilisé presque exclusivement pour accélérer les calculs géométriques. Il avait sa propre mémoire RAM dédiée, 32 KB de SRAM, soit une mémoire locale très rapide. Les transferts entre cette RAM et le reste de l'ordinateur était géré par un contrôleur DMA intégré dans le DSP. En somme, il s'agit d'une sorte de processeur spécialisé dans la géométrie, une sorte d'unité géométrique programmable. Mais la géométrie n'était pas forcément calculée que sur ce DSP, mais pouvait être prise en charge par les 3 CPU. ==L'historique des cartes graphiques des PC, avant l'arrivée de Direct X et Open Gl== Sur PC, l'évolution des cartes graphiques a eu du retard par rapport aux consoles. Les PC sont en effet des machines multi-usage, pour lesquelles le jeu vidéo était un cas d'utilisation parmi tant d'autres. Et les consoles étaient la plateforme principale pour jouer à des jeux vidéo, le jeu vidéo PC étant plus marginal. Mais cela ne veut pas dire que le jeu PC n'existait pas, loin de là ! Un problème pour les jeux PC était que l'écosystème des PC était aussi fragmenté en plusieurs machines différentes : machines Apple 1 et 2, ordinateurs Commdore et Amiga, IBM PC et dérivés, etc. Aussi, programmer des jeux PC n'était pas mince affaire, car les problèmes de compatibilité étaient légion. C'est seulement quand la plateforme x86 des IBM PC s'est démocratisée que l'informatique grand public s'est standardisée, réduisant fortement les problèmes de compatibilité. Mais cela n'a pas suffit, il a aussi fallu que les API 3D naissent. Les API 3D comme Direct X et Open GL sont absolument cruciales pour garantir la compatibilité entre plusieurs ordinateurs aux cartes graphiques différentes. Aussi, l'évolution des cartes graphiques pour PC s'est faite main dans la main avec l'évolution des API 3D. Les fonctionnalités des cartes graphiques ont évolué dans le temps, en suivant les évolutions des API 3D. Du moins dans les grandes lignes, car il est arrivé plusieurs fois que des fonctionnalités naissent sur les cartes graphiques, pour que les fabricants forcent la main de Microsoft ou d'Open GL pour les intégrer de force dans les API 3D. Passons. ===L'introduction des premiers jeux 3D : Quake et les drivers miniGL=== L'API OpenGL est née de la main de SGI, encore eux ! SGI avait créé l'API Iris GL pour ses stations de travail Iris Graphics. Iris GL a ensuite été libéré et est devenu le standard Open GL. Open GL existait déjà avant l'apparition des cartes accélératrices 3D. Il y a avait donc déjà un terreau que les programmeurs graphiques pouvaient utiliser. Mais Open GL était surtout utilisé pour des applications industrielles, médicales (imagerie), graphiques ou militaires, pas pour le jeu vidéo. Mais cela changea avec la sortie du jeu Quake, d'IdSoftware, en 1996. Quake pouvait fonctionner en rendu logiciel, mais le programmeur responsable du moteur 3D (le célébre John Carmack) ajouta une version OpenGL du jeu. Il faut dire que le jeu était programmé sur une station de travail compatible avec OpenGL, même si aucune carte accélératrice de l'époque ne supportait OpenGL. C'était là un choix qui se révéla visionnaire. En théorie, le rendu par OpenGL aurait dû se faire intégralement en logiciel, sauf sur quelques rares stations de travail adaptées. Mais les premières cartes graphiques étaient déjà dans les starting blocks. La toute première carte 3D pour PC est la '''Rendition Vérité V1000''', sortie en Septembre 1995, soit quelques mois avant l'arrivée de la Nintendo 64. La Rendition Vérité V1000 contenait un processeur MIPS cadencé à 25 MHz, 4 mébioctets de RAM, une ROM pour le BIOS, et un RAMDAC, rien de plus. C'était un vrai ordinateur complètement programmable de bout en bout, sans aucun circuit fixe. Les programmeurs ne pouvaient cependant pas utiliser cette programmabilité avec des ''shaders'', mais elle permettait à Rendition d'implémenter n'importe quelle API 3D, que ce soit OpenGL, DirectX ou même sa son API propriétaire. La Rendition Vérité avait de bonnes performances pour ce qui est de la géométrie, mais pas pour le reste. Réaliser la rastérisation et le placage de texture en logiciel n'est pas efficace, pareil pour les opérations de fin de pipeline comme l'antialiasing. Le manque d'unités fixes très rapides pour la rastérisation, le placage de texture ou les opérations de fin de pipeline était clairement un gros défaut. Mais la Rendition Vérité était un cas à part, une exception dans le paysage des cartes 3D de l'époque, qui ne faisait rien comme les autres. Les autres cartes graphiques, sorties peu après, étaient les Voodoo de 3dfx, les Riva TNT de NVIDIA, les Rage/3D d'ATI, la Virge/3D de S3, et la Matrox Mystique. Elles avaient choisit le compromis inverse de la Rendition Vérité V1000 : de bonnes performances pour le placage de textures et la rastérization, mais pas pour les calculs géométriques. Pour rappel, les systèmes professionnels et les consoles avaient des processeurs pour la géométrie, et des circuits fixes pour le reste. Les cartes graphiques de PC se passaient des processeurs pour la géométrie, les calculs géométriques étaient réalisés par le CPU. Les toutes premières cartes 3D pour PC contenaient seulement des circuits pour gérer les textures et des ROPs. Elle géraient le ''z-buffer'' en mémoire vidéo, ainsi que des effets de brouillard. Il n'y avait même pas de circuit pour la rastérisation, qui était faite en logiciel, avec les calculs géométriques. [[File:Architecture de base d'une carte 3D - 2.png|centre|vignette|upright=1.5|Carte 3D sans rasterization matérielle.]] Les cartes suivantes ajoutèrent une gestion des étapes de ''rasterization'' directement en matériel. Les cartes ATI rage 2, les Invention de chez Rendition, et d'autres cartes graphiques supportaient la rasterisation en hardware. [[File:Architecture de base d'une carte 3D - 3.png|centre|vignette|upright=1.5|Carte 3D avec gestion de la géométrie.]] Pour exploiter les unités de texture et le circuit de rastérisation, OpenGL et Direct 3D étaient partiellement implémentées en logiciel, car les cartes graphiques ne supportaient pas toutes les fonctionnalités de l'API. C'était l'époque du miniGL, des implémentations partielles d'OpenGL, fournies par les fabricants de cartes 3D, implémentées dans les pilotes de périphériques de ces dernières. Les fonctionnalités d'OpenGL implémentées dans ces pilotes étaient presque toutes exécutées en matériel, par la carte graphique. Avec l'évolution du matériel, les pilotes de périphériques devinrent de plus en plus complets, au point de devenir des implémentations totales d'OpenGL. Mais au-delà d'OpenGL, chaque fabricant de carte graphique avait sa propre API propriétaire, qui était gérée par leurs pilotes de périphériques (''drivers''). Par exemple, les premières cartes graphiques de 3dfx interactive, les fameuses voodoo, disposaient de leur propre API graphique, l'API Glide. Elle facilitait la gestion de la géométrie et des textures, ce qui collait bien avec l'architecture de ces cartes 3D. Mais ces API propriétaires tombèrent rapidement en désuétude avec l'évolution de DirectX et d'OpenGL. Direct X était une API dans l'ombre d'Open GL. La première version de Direct X qui supportait la 3D était DirectX 2.0 (juin 2, 1996), suivie rapidement par DirectX 3.0 (septembre 1996). Elles dataient d'avant le jeu Quake, et elles étaient très éloignées du hardware des premières cartes graphiques. Elles utilisaient un système d'''execute buffer'' pour communiquer avec la carte graphique, Microsoft espérait que le matériel 3D implémenterait ce genre de système. Ce qui ne fu pas le cas. Direct X 4.0 a été abandonné en cours de développement pour laisser à une version 5.0 assez semblable à la 2.0/3.0. Le mode de rendu laissait de côté les ''execute buffer'' pour coller un peu plus au hardware de l'époque. Mais rien de vraiment probant comparé à Open GL. Même Windows utilisait Open GL au lieu de Direct X maison... C'est avec Direct X 6.0 que Direct X est entré dans la cours des grands. Il gérait la plupart des technologies supportées par les cartes graphiques de l'époque. ===Le ''multi-texturing'' de l'époque Direct X 6.0 : combiner plusieurs textures=== Une technologie très importante standardisée par Dirext X 6 est la technique du '''''multi-texturing'''''. Avec ce qu'on a dit dans le chapitre précédent, vous pensez sans doute qu'il n'y a qu'une seule texture par objet, qui est plaquée sur sa surface. Mais divers effet graphiques demandent d'ajouter des textures par dessus d'autres textures. En général, elles servent pour ajouter des détails, du relief, sur une surface pré-existante. Un exemple intéressant vient des jeux de tir : ajouter des impacts de balles sur les murs. Pour cela, on plaque une texture d'impact de balle sur le mur, à la position du tir. Il s'agit là d'un exemple de '''''decals''''', des petites textures ajoutées sur les murs ou le sol, afin de simuler de la poussière, des impacts de balle, des craquelures, des fissures, des trous, etc. Les textures en question sont de petite taille et se superposent à une texture existante, plus grande. Rendre des ''decals'' demande de pouvoir superposer deux textures. Direct X 6.0 supportait l'application de plusieurs textures directement dans le matériel. La carte graphique devait être capable d'accéder à deux textures en même temps, ou du moins faire semblant que. Pour cela, elle doublaient les unités de texture et adaptaient les connexions entre unités de texture et mémoire vidéo. La mémoire vidéo devait être capable de gérer plusieurs accès mémoire en même temps et devait alors avoir un débit binaire élevé. [[File:Multitexturing.png|centre|vignette|upright=2|Multitexturing]] La carte graphique devait aussi gérer de quoi combiner deux textures entre elles. Par exemple, pour revenir sur l'exemple d'une texture d'impact de balle, il faut que la texture d'impact recouvre totalement la texture du mur. Dans ce cas, la combinaison est simple : la première texture remplace l'ancienne, là où elle est appliquée. Mais les cartes graphiques ont ajouté d'autres combinaisons possibles, par exemple additionner les deux textures entre elle, faire une moyenne des texels, etc. Les opérations pour combiner les textures était le fait de circuits appelés des '''''combiners'''''. Concrètement, les ''combiners'' sont de simples unités de calcul. Les ''conbiners'' ont beaucoup évolués dans le temps, mais les premières implémentation se limitaient à quelques opérations simples : addition, multiplication, superposition, interpolation. L'opération effectuer était envoyée au ''conbiner'' sur une entrée dédiée. [[File:Multitexturing avec combiners.png|centre|vignette|upright=2|Multitexturing avec combiners]] S'il y avait eu un seul ''conbiner'', le circuit de ''multitexturing'' aurait été simplement configurable. Mais dans la réalité, les premières cartes utilisant du ''multi-texturing'' utilisaient plusieurs ''combiners'' placés les uns à la suite des autres. L'implémentation des ''combiners'' retenue par Open Gl, et par le hardware des cartes graphiques, était la suivante. Les ''combiners'' étaient placés en série, l'un à la suite de l'autre, chacun combinant le résultat de l'étage précédent avec une texture. Le premier ''combiner'' gérait l'éclairage par sommet, afin de conserver un minimum de rétrocompatibilité. [[File:Texture combiners Open GL.png|centre|vignette|upright=2|Texture combiners Open GL]] Voici les opérations supportées par les ''combiners'' d'Open GL. Ils prennent en entrée le résultat de l'étage précédent et le combinent avec une texture lue depuis l'unité de texture. {|class="wikitable" |+ Opérations supportées par les ''combiners'' d'Open GL |- ! Replace | colspan="2" | Pixel provenant de l'unité de texture |- ! Addition | colspan="2" | Additionne l'entrée au texel lu. |- ! Modulate | colspan="2" | Multiplie l'entrée avec le texel lu |- ! Mélange (''blending'') | Moyenne pondérée des deux entrées, pondérée par la composante de transparence || La couleur de transparence du texel lu et de l'entrée sont multipliées. |- ! Decals | Moyenne pondérée des deux entrées, pondérée par la composante de transparence. || La transparence du résultat est celle de l'entrée. |} Il faut noter qu'un dernier étage de ''combiners'' s'occupait d'ajouter la couleur spéculaire et les effets de brouillards. Il était à part des autres et n'était pas configurable, c'était un étage fixe, qui était toujours présent, peu importe le nombre de textures utilisé. Il était parfois appelé le '''''combiner'' final''', terme que nous réutiliserons par la suite. Mine de rien, cela a rendu les cartes graphiques partiellement programmables. Le fait qu'il y ait des opérations enchainées à la suite, opérations qu'on peut choisir librement, suffit à créer une sorte de mini-programme qui décide comment mélanger plusieurs textures. Mais il y avait une limitation de taille : le fait que les données soient transmises d'un étage à l'autre, sans détours possibles. Par exemple, le troisième étage ne pouvait avoir comme seule opérande le résultat du second étage, mais ne pouvait pas utiliser celui du premier étage. Il n'y avait pas de registres pour stocker ce qui sortait de la rastérisation, ni pour mémoriser temporairement les texels lus. ===Le ''Transform & Lighting'' matériel de Direct X 7.0=== [[File:Architecture de base d'une carte 3D - 4.png|vignette|upright=1.5|Carte 3D avec gestion de la géométrie.]] La première carte graphique pour PC capable de gérer la géométrie en hardware fût la Geforce 256, la toute première Geforce. Son unité de gestion de la géométrie n'est autre que la bien connue '''unité T&L''' (''Transform And Lighting''). Elle implémentait des algorithmes d'éclairage de la scène 3D assez simples, comme un éclairage de Gouraud, qui étaient directement câblés dans ses circuits. Mais contrairement à la Nintendo 64 et aux bornes d'arcade, elle implémentait le tout, non pas avec un processeur classique, mais avec des circuits fixes. Avec Direct X 7.0 et Open GL 1.0, l'éclairage était en théorie limité à de l'éclairage par sommet, l'éclairage par pixel n'était pas implémentable en hardware. Les cartes graphiques ont tenté d'implémenter l'éclairage par pixel, mais cela n'est pas allé au-delà du support de quelques techniques de ''bump-mapping'' très limitées. Par exemple, Direct X 6.0 implémentait une forme limitée de ''bump-mapping'', guère plus. Un autre problème est qu'il a beaucoup d'algorithmes d'éclairages différents, aux résultats visuels différents, bien au-delà des algorithmes d'éclairage plat, de Gouraud et de Phong. Et les unités de T&L étaient souvent en retard sur les algorithmes logiciels. Les programmeurs avaient le choix entre programmer les algorithmes d’éclairage qu'ils voulaient et les exécuter en logiciel, ou utiliser ceux de l'unité de T&L. Ils choisissaient souvent la première option. Par exemple, Quake 3 Arena et Unreal Tournament n'utilisaient pas les capacités d'éclairage géométrique et préféraient utiliser leurs calculs d'éclairage logiciel fait maison. Cependant, le hardware dépassait les capacités des API et avait déjà commencé à ajouter des capacités de programmation liées au ''multi-texturing''. Les cartes graphiques de l'époque, surtout chez NVIDIA, implémentaient un système de '''''register combiners''''', une forme améliorée de ''texture combiners'', qui permettait de faire une forme limitée d'éclairage par pixel, notamment du vrai ''bump-mampping'', voire du ''normal-mapping''. Mais ce n'était pas totalement supporté par les API 3D de l'époque. Les ''registers combiners'' sont des ''texture combiners'' mais dans lesquels ont aurait retiré la stricte organisation en série. Il y a toujours plusieurs étages à la suite, qui peuvent exécuter chacun une opération, mais tous les étages ont maintenant accès à toutes les textures lues et à tout ce qui sort de la rastérisation, pas seulement au résultat de l'étape précédente. Pour cela, on ajoute des registres pour mémoriser ce qui sort des unités de texture, et pour ce qui sort de la rastérisation. De plus, on ajoute des registres temporaires pour mémoriser les résultats de chaque ''combiner'', de chaque étage. Il faut cependant signaler qu'il existe un ''combiner'' final, séparé des étages qui effectuent des opérations proprement dits. Il s'agit de l'étage qui applique la couleur spéculaire et les effets de brouillards. Il ne peut être utilisé qu'à la toute fin du traitement, en tant que dernier étage, on ne peut pas mettre d'opérations après lui. Sa sortie est directement connectée aux ROPs, pas à des registres. Il faut donc faire la distinction entre les '''''combiners'' généraux''' qui effectuent une opération et mémorisent le résultat dans des registres, et le ''combiner'' final qui envoie le résultat aux ROPs. L'implémentation des ''register combiners'' utilisait un processeur spécialisés dans les traitements sur des pixels, une sorte de proto-processeur de ''shader''. Le processeur supportait des opérations assez complexes : multiplication, produit scalaire, additions. Il s'agissait d'un processeur de type VLIW, qui sera décrit dans quelques chapitres. Mais ce processeur avait des programmes très courts. Les premières cartes NVIDIA, comme les cartes TNT pouvaient exécuter deux opérations à la suite, suivie par l'application de la couleurs spéculaire et du brouillard. En somme, elles étaient limitées à un ''shader'' à deux/trois opérations, mais c'était un début. Le nombre d'opérations consécutives est rapidement passé à 8 sur la Geforce 3. ===L'arrivée des ''shaders'' avec Direct X 8.0=== [[File:Architecture de la Geforce 3.png|vignette|upright=1.5|Architecture de la Geforce 3]] Les ''register combiners'' était un premier pas vers un éclairage programmable. Paradoxalement, l'évolution suivante s'est faite non pas dans l'unité de rastérisation/texture, mais dans l'unité de traitement de la géométrie. La Geforce 3 a remplacé l'unité de T&L par un processeur capable d'exécuter des programmes. Les programmes en question complétaient l'unité de T&L, afin de pouvoir rajouter des techniques d'éclairage plus complexes. Le tout a permis aussi d'ajouter des animations, des effets de fourrures, des ombres par ''shadow volume'', des systèmes de particule évolués, et bien d'autres. À partir de la Geforce 3 de Nvidia, les cartes graphiques sont devenues capables d'exécuter des programmes appelés '''''shaders'''''. Le terme ''shader'' vient de ''shading'' : ombrage en anglais. Grace aux ''shaders'', l'éclairage est devenu programmable, il n'est plus géré par des unités d'éclairage fixes mais été laissé à la créativité des programmeurs. Les programmeurs ne sont plus vraiment limités par les algorithmes d'éclairage implémentés dans les cartes graphiques, mais peuvent implémenter les algorithmes d'éclairage qu'ils veulent et peuvent le faire exécuter directement sur la carte graphique. Les ''shaders'' sont classifiés suivant les données qu'ils manipulent : '''''pixel shader''''' pour ceux qui manipulent des pixels, '''''vertex shaders''''' pour ceux qui manipulent des sommets. Les premiers sont utilisés pour implémenter l'éclairage par pixel, les autres pour gérer tout ce qui a trait à la géométrie, pas seulement l'éclairage par sommets. Direct X 8.0 avait un standard pour les shaders, appelé ''shaders 1.0'', qui correspondait parfaitement à ce dont était capable la Geforce 3. Il standardisait les ''vertex shaders'' de la Geforce 3, mais il a aussi renommé les ''register combiners'' comme étant des ''pixel shaders'' version 1.0. Les ''register combiners'' n'ont pas évolués depuis la Geforce 256, si ce n'est que les programmes sont passés de deux opérations successives à 8, et qu'il y avait possibilité de lire 4 textures en ''multitexturing''. A l'opposé, le processeur de ''vertex shader'' de la Geforce 3 était capable d'exécuter des programmes de 128 opérations consécutives et avait 258 registres différents ! Des ''pixels shaders'' plus évolués sont arrivés avec l'ATI Radeon 8500 et ses dérivés. Elle incorporait la technologie ''SMARTSHADER'' qui remplacait les ''registers combiners'' par un processeur de ''shader'' un peu limité. Un point est que le processeur acceptait de calculer des adresses de texture dans le ''pixel shader''. Avant, les adresses des texels à lire étaient fournis par l'unité de rastérisation et basta. L'avantage est que certains effets graphiques étaient devenus possibles : du ''bump-mapping'' avancé, des textures procédurales, de l'éclairage par pixel anisotrope, du éclairage de Phong réel, etc. Avec la Radeon 8500, le ''pixel shader'' pouvait calculer des adresses, et lire les texels associés à ces adresses calculées. Les ''pixel shaders'' pouvaient lire 6 textures, faire 8 opérations sur les texels lus, puis lire 6 textures avec les adresses calculées à l'étape précédente, et refaire 8 opérations. Quelque chose de limité, donc, mais déjà plus pratique. Les ''pixel shaders'' de ce type ont été standardisé dans Direct X 8.1, sous le nom de ''pixel shaders 1.4''. Encore une fois, le hardware a forcé l'intégration dans une API 3D. ===Les ''shaders'' de Direct X 9.0 : de vrais ''pixel shaders''=== Avec Direct X 9.0, les ''shaders'' sont devenus de vrais programmes, sans les limitations des ''shaders'' précédents. Les ''pixels shaders'' sont passés à la version 2.0, idem pour les ''vertex shaders''. Concrètement, ils ont des fonctionnalités bien supérieures à celles des ''registers combiners''. Les ''shaders'' pouvaient exécuter une suite d'opérations arbitraire, dans le sens où elle n'était pas structurée avec tel type d'opération au début, suivie par un accès aux textures, etc. On pouvait mettre n'importe quelle opération dans n'importe quel ordre. De plus, les ''shaders'' ne sont plus écrit en assembleur comme c'était le cas avant. Ils sont dorénavant écrits dans un langage de haut-niveau, le HLSL pour les shaders Direct X et le GLSL pour les shaders Open Gl. Les ''shaders'' sont ensuite traduit (compilés) en instructions machines compréhensibles par la carte graphique. Au début, ces langages et la carte graphique supportaient uniquement des opérations simples. Mais au fil du temps, les spécifications de ces langages sont devenues de plus en plus riches à chaque version de Direct X ou d'Open Gl, et le matériel en a fait autant. Le matériel s'est alors adapté, en incorporant un véritable processeur pour les ''pixel shaders''. Les ''pixel shaders'' sont maintenant exécutés par un processeur de ''shader'' dédié, aux fonctionnalités bien supérieures à celles des ''registers combiners''. Le processeur de ''pixel shader'' incorpore l'unité de texture en sont sein, les deux sont fusionnés. La raison à cela sera expliqué dans la suite du chapitre. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=1.5|Carte 3D avec pixels et vertex shaders non-unifiés.]] ===L'après Direct X 9.0=== Avant Direct X 10, les processeurs de ''shaders'' ne géraient pas exactement les mêmes opérations pour les processeurs de ''vertex shader'' et de ''pixel shader''. Les processeurs de ''vertex shader'' et de ''pixel shader''étaient séparés. Depuis DirectX 10, ce n'est plus le cas : le jeu d'instructions a été unifié entre les vertex shaders et les pixels shaders, ce qui fait qu'il n'y a plus de distinction entre processeurs de vertex shaders et de pixels shaders, chaque processeur pouvant traiter indifféremment l'un ou l'autre. [[File:Architecture de base d'une carte 3D - 6.png|centre|vignette|upright=1.5|Architecture de la GeForce 6800.]] Avec Direct X 10, de nombreux autres ''shaders'' sont apparus. Les plus liés au rendu 3D sont les '''''geometry shader''''' pour ceux qui manipulent des triangles, de ''hull shaders'' et de ''domain shaders'' pour la tesselation. De plus, les cartes graphiques modernes sont capables d’exécuter des programmes informatiques qui n'ont aucun lien avec le rendu 3D, mais sont exécutés par la carte graphique comme le ferait un processeur d'ordinateur normal. De tels ''shaders'' sans lien avec le rendu 3D sont appelés des ''compute shader''. ==Les cartes graphiques d'aujourd'hui== Les circuits d'un GPU ont beaucoup évolué depuis l'introduction des ''shaders'', pour devenir de plus en plus programmables. Mais à côté des processeurs de ''shaders'', il reste quelques circuits non-programmables appelés des circuits fixes. La rastérisation, le placage de texture, l'élimination des pixels cachés et le mélange ''alpha'' sont gérés par des circuits fixes. [[File:3D-Pipeline.svg|centre|vignette|upright=3.0|Pipeline 3D : ce qui est programmable et ce qui ne l'est pas dans une carte graphique moderne.]] Mais pourquoi ne pas tout rendre programmable ? Ou au contraire, utiliser seulement des circuits fixes ? La réponse rapide est qu'il s'agit d'un compromis entre flexibilité et performance qui permet d'avoir le meilleur des deux mondes. Mais ce compromis a fortement évolué dans le temps, comme on va le voir plus bas. Rendre l'éclairage programmable permet d'implémenter facilement un grand nombre d'effets graphiques sans avoir à les implémenter en hardware. Avant les ''shaders'', les effets graphiques derniers cri n'étaient disponibles que sur les derniers modèles de carte graphique. Avec des ''vertex/pixel shaders'', ce genre de défaut est passé à la trappe. Si un nouvel algorithme de rendu graphique est inventé, il peut être utilisé dès le lendemain sur toutes les cartes graphiques modernes. De plus, implémenter beaucoup d'algorithmes d'éclairage différents avec des circuits fixes a un cout en termes de transistors, alors qu'utiliser des circuits programmable a un cout en hardware plus limité. Tout cela est à l'exact opposé de ce qu'on a avec les autres circuits, comme les circuits pour la rastérisation ou le placage de texture. Il n'y a pas 36 façons de rastériser une scène 3D et la flexibilité n'est pas un besoin important pour cette opération, alors que les performances sont cruciales. Même chose pour le placage/filtrage de textures. En conséquences, les unités de rastérisation, de texture, et les ROPs sont toutes implémentées en matériel. Faire ainsi permet de gagner en performance sans que cela ait le moindre impact pour le programmeur. Reste à expliquer dans le détail pourquoi. ===Les unités de texture sont intégrées aux processeurs de shaders=== Avec l'arrivée des processeurs de shaders, les unités de texture ont été intégrées dans les processeurs de shaders eux-mêmes. C'est la seule unité fixe qui a subit ce traitement, et il est intéressant de comprendre pourquoi. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=2|Architecture de base d'une carte 3D.]] Pour cela, il faut faire un rappel sur ce qu'il y a dans un processeur. Un processeur contient globalement quatre circuits : * une unité de calcul qui fait des calculs ; * des registres pour stocker les opérandes et résultats des calculs ; * une unité de communication avec la mémoire ; * et un séquenceur, un circuit de contrôle qui commande les autres. L'unité de communication avec la mémoire sert à lire ou écrire des données, à les transférer de la RAM vers les registres, ou l'inverse. Lire une donnée demande d'envoyer son adresse à la RAM, qui répond en envoyant la donnée lue. Elle est donc toute indiquée pour lire une texture : lire une texture n'est qu'un cas particulier de lecture de données. Les texels à lire sont à une adresse précise, la RAM répond à la lecture avec le texel demandé. Il est donc possible d'utiliser l'unité de communication avec la mémoire comme si c'était une unité de texture. Cependant, les textures ne sont pas utilisées comme telles de nos jours. Le rendu 3D moderne utilise des techniques dites de filtrage de texture, qui permettent d'améliorer la qualité du rendu des textures. Sans ce filtrage de texture, les textures appliquées naïvement donnent un résultat assez pixelisé et assez moche, pour des raisons assez techniques. Le filtrage élimine ces artefacts, en utilisant une forme d'''antialiasing'' interne aux textures, le fameux filtrage de texture. Le filtrage de texture peut être réalisé en logiciel ou en matériel. Techniquement, il est possible de le faire dans un ''shader''. Le ''shader'' calcule les adresses des texels à lire, lit les texels, et effectue ensuite le filtrage avec des opérations de calcul. Mais ce n'est pas ce qui est fait, le filtrage de texture est toujours effectué directement en matériel. La raison est que le filtrage de texture est très simple à implémenter en hardware. Le filtrage bilinéaire ou trilinéaire demande juste des circuits d'interpolation et quelques registres, ce qui est trivial. Et la seconde raison est qu'il n'y a pas 36 façons de filtrer des textures : une carte graphique peut implémenter les algorithmes principaux existants en assez peu de circuits. Pour simplifier l'implémentation, les processeurs de ''shader'' modernes disposent d'une unité d'accès mémoire séparée de l'unité de texture. L'unité d'accès mémoire normale s'occupe des accès mémoire hors-textures, alors que l'unité mémoire s'occupe de lire les textures. L'unité de texture contient de quoi faire du filtrage de texture, mais aussi faire des calculs d'adresse spécialisées, intrinsèquement liés au format des textures, qu'on détaillera dans le chapitre sur les textures. En comparaison, les unités d'accès mémoire effectuent des calculs d'adresse plus basiques. Un dernier avantage est que l'unité de texture est reliée au cache de texture, alors que l'unité d'accès mémoire est relié au cache L1/L2. ===Les ROPs peuvent être implémentés dans le ''pixel shader''=== Il faut noter que les ROPs peuvent aussi être intégré dans les processeurs de ''shader'', mais c'est assez rare. Les cartes graphiques pour PC ont des ROPs séparés des processeurs de ''shaders''. Par contre, quelques cartes graphiques destinées les smartphones et autres appareils mobiles font exception. Sur celles-ci, les unités de textures et les ROPs sont intégrés dans les processeurs de ''shaders'', aux côtés de l'unité d'accès mémoire LOAD/STORE. Il s'agit de cartes graphiques en rendu à tuiles. {{NavChapitre | book=Les cartes graphiques | prev=Les cartes graphiques : architecture de base | prevText=Les cartes graphiques : architecture de base | next=Les processeurs de shaders | nextText=Les processeurs de shaders }} {{autocat}} 5g641sxf941to5lw44pudtbq22e691o 763504 763503 2026-04-11T19:34:43Z Mewtow 31375 /* Les ROPs peuvent être implémentés dans le pixel shader */ 763504 wikitext text/x-wiki Il est intéressant d'étudier le hardware des cartes graphiques en faisant un petit résumé de leur évolution dans le temps. En effet, leur hardware a fortement évolué dans le temps. Et il serait difficile à comprendre le hardware actuel sans parler du hardware d'antan. En effet, une carte graphique moderne est partiellement programmable. Certains circuits sont totalement programmables, d'autres non. Et pour comprendre pourquoi, il faut étudier comment ces circuits ont évolués. Le hardware des cartes graphiques a fortement évolué dans le temps, ce qui n'est pas une surprise. Les évolutions de la technologie, avec la miniaturisation des transistors et l'augmentation de leurs performances a permis aux cartes graphiques d'incorporer de plus en plus de circuits avec les années. Avant l'invention des cartes graphiques, toutes les étapes du pipeline graphique étaient réalisées par le processeur : il calculait l'image à afficher, et l’envoyait à une carte d'affichage 2D. Au fil du temps, de nombreux circuits furent ajoutés, afin de déporter un maximum de calculs vers la carte vidéo. Le rendu 3D moderne est basé sur le placage de texture inverse, avec des coordonnées de texture, une correction de perspective, etc. Mais les anciennes consoles et bornes d'arcade utilisaient le placage de texture direct. Et cela a impacté le hardware des consoles/PCs de l'époque. Avec le placage de texture direct, il était primordial de calculer la géométrie, mais la rasterisation était le fait de VDC améliorés. Aussi, les premières bornes d'arcade 3D et les consoles de 5ème génération disposaient processeurs pour calculer la géométrie et de circuits d'application de textures très particuliers. A l'inverse, les PC utilisaient un rendu inverse, totalement différent. Sur les PC, les premières cartes graphiques avaient un circuit de rastérisation et des unités de textures, mais pas de circuits géométriques. ==Les premières cartes graphiques== Dès les années 70-80, le rendu 3D était utilisé par de nombreuses entreprises industrielles : des applications de visualisation 3D étaient utilisées en architecture, des applications de conception assistée par ordinateur étaient déjà d'utilisation courante, sans compter les simulateurs de vol utilisés par l'armée et les instructeurs qui formaient les pilotes d'avion. Le rendu 3D était aussi étudié au niveau académique, la recherche en 3D était déjà florissante. Il existait même du matériel spécifiquement conçu pour le rendu graphique, mais celui-ci était spécifiquement dédié à des super-calculateurs ou des ''workstations'' (des sortes d'ancêtres des PC, très puissants pour l'époque, mais conçus uniquement pour les entreprises). ===Le début des années 80 : le rendu en fils de fer=== Le tout premier système de ce genre était le '''''Line Drawing System-1''''' de l'entreprise Evans & Sutherland, daté de 1969. Ce n'est ni plus ni moins que le toute premier circuit graphique séparé du processeur ayant existé. C'est en un sens la toute première carte graphique, le tout premier GPU. Il prenait la forme d'un périphérique qui se connectait à l'ordinateur d'un côté et était relié à l'écran de l'autre. Il était compatible avec un grand nombre d'ordinateurs et de processeurs existants. Il a été suivi par plusieurs successeurs, nommés ''Picture System 1, 2'' et le ''PS300 series''. [[File:Evans & Sutherland LDS-1 (1).jpg|vignette|Evans & Sutherland LDS-1 (1)]] Ils permettaient de faire du rendu en fil de fer, sans texture ni même sans polygones colorés. Un tel rendu était utile pour des applications assez limitées : architecture, dessin de molécules pour les entreprises pharmaceutique et certains centres de recherche, l'aérospatiale, etc. Ces cartes graphiques étaient utilisées de concert avec des écrans appelés '''écrans vectoriels''' (''vector display''). Pour simplifier, ils ressemblaient à des écrans CRT, sauf que le faisceau d'électron ne balayait pas l'écran ligne par ligne, mais traçait des lignes arbitraires à l'écran. On lui précisait deux points de coordonnées x1,y1 ; et x2,y2 ; puis l'écran tracait une ligne entre ces deux points. En général, la ligne tracée était maintenue pendant un long moment, entre plusieurs secondes et plusieurs minutes. L'intérieur du circuit était assez simple : un circuit de multiplication de matrice pour les calculs géométriques, un rastériser simplifié (le ''clipping diviser''), un circuit de tracé de lignes, et un processeur de contrôle pour commander les autres circuits. Le fait que ces trois circuits soient séparés permettait une implémentation en pipeline, où plusieurs portions de l'image pouvaient être calculées en même temps : pendant que l'une est dans l'unité géométrique, l'autre est dans le rastériseur et une troisième est en cours de tracé. [[File:Lds1blockdiagram05.svg|centre|vignette|upright=2|Architecture du LDS-1. Le processeur de contrôle n'est pas représenté.]] Le processeur de contrôle exécute un programme qui se charge de commander l'unité géométrique et les autres circuits. Le programme en question est fourni par le programmeur, le LDS-1 est donc totalement programmable. Il lit directement les données nécessaires pour le rendu dans la mémoire de l’ordinateur et le programme exécuté est lui aussi en mémoire principale. Il n'a pas de mémoire vidéo dédiée, il utilise la RAM de l'ordinateur principal. Le multiplieur de matrices est plus complexe qu'on pourrait s'y attendre. Il ne s'agit pas que d'un circuit arithmétique tout simple, mais d'un véritable processeur avec des registres et des instructions machine complexes. Il contient plusieurs registres, l'ensemble mémorisant 4 matrices de 16 nombres chacune (4 lignes de 4 colonnes). Un nombre est codé sur 18 bits. Les registres sont reliés à un ensemble de circuits arithmétiques, des additionneurs et des multiplieurs. Le circuit supporte des instructions de copie entre registres, pour copier une ligne d'une matrice à une autre, des instructions LOAD/STORE pour lire ou écrire dans la mémoire RAM, etc. Il supporte aussi des multiplications en 2D et 3D. Le ''clipping divider'' est un circuit assez complexe, contenant un processeur à accumulateur, une mémoire ROM pour le programme du processeur. Le programme exécuté par le processeur est un petit programme de 62 instructions, stocké dans la ROM. L'algorithme du ''clipping divider'' est décrite dans le papier de recherche "A clipping divider", écrit par Robert Sproull. Un détail assez intéressant est que le résultat en sortie de l'unité géométrique et du rastériseur peuvent être envoyés à l'ordinateur en parallèle du rendu. C'était très utile sur les anciens ordinateurs qui étaient connectés à plusieurs terminaux. Le LDS-1 calculait la géométrie et le rendu, et le tout pouvait petre envoyé à d'autres composants, comme des terminaux, une imprimante, etc. ===Les systèmes ultérieurs : rendu à triangles colorés et texturé=== Les systèmes précédents étaient très limités : ils calculaient la géométrie et n'avaient pas de ''framebuffer'', ni de tampon de profondeur, ni gestion de l'éclairage, ni quoique ce soit. De tels systèmes étaient donc des accélérateurs géométriques que de vrais systèmes graphiques complets, du fait de l'absence de ''framebuffer''. Ils étaient composés de processeurs spécialisés dans les calculs à virgule flottante, faisant des calculs géométriques, et éventuellement d'un processeur pour la rastérisation. La raison est que la RAM était très chère et que créer des circuits fixes étaient très chers et peu disponibles. Par contre, les processeurs à virgule flottante étaient peu chers et facile à trouver. Vers la fin des années 80, grâce à la baisse du prix de la RAM et la démocratisation des ASIC (des circuits fixes fait sur mesure), ajouter un ''framebuffer'' est est devenu possible. C'est alors que sont apparus les '''systèmes de rendu 3D de première génération'''. De tels systèmes ont permis d'implémenter le rendu à primitives colorées qu'on a vu il y a quelques chapitres, à savoir un rendu où les triangles sont coloriés avec une couleur unique. Les systèmes de première génération étaient simples : des processeurs pour le calcul de la géométrie, un circuit de rastérisation, une RAM pour le ''framebuffer'' et des ASIC servant de ROPs très simples. Il n'y avait pas d'élimination des pixels cachés, pas de textures, et encore moins d'éclairage par pixels. Le premier système de ce genre était le ''Shaded Picture System'', toujours par Evans & Sutherland. Il ne gérait pas la couleur et ne pouvait afficher que des images en noir et blanc, mais il gérait l'éclairage par sommet (''vertex lighting''). Il a rapidement été dépassé par les systèmes de l'entreprise ''Silicon Graphics Inc'' (SGI), ainsi que ceux de l'entreprise Apollo avec sa série Apollo DN. Les '''systèmes de seconde génération''' sont apparus vers la fin des années 80, et se distinguent des précédents par l'ajout un tampon de profondeur. Ils intègrent aussi des capacités d'éclairage par pixel, à savoir de l'éclairage plat, de Gouraud, voire de Phong ! Enfin, les '''systèmes de troisième génération''' ont acquis des capacités de placage de texture, que les systèmes précédents n'avaient pas. Ils ont aussi ajouté un support de l'antialiasing. Les systèmes SGI avec placage de texture ont déjà été abordé au chapitre précédent, dans la section sur les GPU en mode immédiat et à ''tile''. Aussi, nous ne reviendrons pas dessus. [[File:Evolution de l'architecture des premières cartes graphiques, dans les années 80-90.png|centre|vignette|upright=2.5|Evolution de l'architecture des premières cartes graphiques, dans les années 80-90]] Les systèmes de première, seconde et troisième génération avaient de nombreux points communs. En premier lieu, ils étaient fabriqués en connectant plusieurs cartes électroniques : une carte pour les calculs géométriques, une ou plusieurs cartes pour le reste du rendu graphique, une carte dédiée au VDC et avec un connecteur écran. Les transistors de l'époque n'étaient pas encore miniaturisés, ce qui fait que le système graphique ne pouvait pas tenir sur une seule carte électronique. Il n'y avait donc pas de carte graphique proprement dit, mais un équivalent éclaté sur plusieurs cartes électroniques. La carte pour la géométrie contenait typiquement une mémoire FIFO pour accumuler les commandes de rendu, un processeur de commande, et plusieurs processeurs géométriques. Les processeurs géométriques étaient parfois conçus sur mesure, comme l'a été le le ''Geometry Engine'' de SGI. Mais il est arrivé qu'ils utilisent des processeurs commerciaux comme le Weitek 3222, l'Intel i860, etc. Les processeurs pouvaient être placés en série ou en parallèle, comme expliqué dans le chapitre précédent. Le circuit de rastérisation était réalisé soit avec un processeur dédié, soit avec un circuit fixe, soit un mélange des deux. La rastérisation est en effet réalisée en plusieurs étapes, certaines peuvent être implémentées avec un processeur et d'autres avec des circuits fixes. Un point important est qu'à l'époque, le rendu n'utilisait pas que des triangles, mais des polygones en général. Ce n'est que par la suite que le rendu s'est focalisé sur les triangles et les ''quads'' (quadrilatères). Il arrivait que le système graphique gérait partiellement des polygones concaves, voire convexes. Sur les systèmes SGI, les calculs géométriques se faisaient avec des polygones, que la rastérisation découpait en triangles, le reste du rendu se faisait avec des triangles. Les stations de travail Apollo DN 10000VS découpaient les polygones en trapézoïdes orientés à l'horizontale, alignés avec des ''scanlines''. D'autres systèmes découpaient tout en triangle lors de l'étape géométrique ==Les précurseurs grand public : les bornes d'arcade== [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] L'accélération du rendu 3D sur les bornes d'arcade était déjà bien avancé dès les années 90. Les bornes d'arcade ont toujours été un segment haut de gamme de l'industrie du jeu vidéo, aussi ce n'est pas étonnant. Le prix d'une borne d'arcade dépassait facilement les 10 000 dollars pour les plus chères et une bonne partie du prix était celui du matériel informatique. Le matériel était donc très puissant et débordait de mémoire RAM comparé aux consoles de jeu et aux PC. La plupart des bornes d'arcade utilisaient du matériel standardisé entre plusieurs bornes. A l'intérieur d'une borne d'arcade se trouve une '''carte de borne d'arcade''' qui est une carte mère avec un ou plusieurs processeurs, de la RAM, une carte graphique, un VDC et pas mal d'autres matériels. La carte est reliée aux périphériques de la borne : joysticks, écran, pédales, le dispositif pour insérer les pièces afin de payer, le système sonore, etc. Le jeu utilisé pour la borne est placé dans une cartouche qui est insérée dans un connecteur spécialisé. Les cartes de bornes d'arcade étaient généralement assez complexes, elles avaient une grande taille et avaient plus de composants que les cartes mères de PC. Chaque carte contenait un grand nombre de chips pour la mémoire RAM et ROM, et il n'était pas rare d'avoir plusieurs processeurs sur une même carte. Et il n'était pas rare d'avoir trois à quatre cartes superposées dans une seule borne. Pour ceux qui veulent en savoir plus, Fabien Sanglard a publié gratuitement un livre sur le fonctionnement des cartes d'arcade CPS System, disponible via ce lien : [https://fabiensanglard.net/b/cpsb.pdf The book of CP System]. Les premières cartes graphiques des bornes d'arcade étaient des cartes graphiques 2D auxquelles on avait ajouté quelques fonctionnalités. Les sprites pouvaient être tournés, agrandit/réduits, ou déformés pour simuler de la perspective et faire de la fausse 3D. Par la suite, le vrai rendu 3D est apparu sur les bornes d'arcade. Dès 1988, la carte d'arcade Namco System 21 et Sega Model 1 géraient les calculs géométriques. Quelques années plus tard, les cartes graphiques se sont mises à supporter un éclairage de Gouraud et du placage de texture. Par exemple, le Namco System 22 et la Sega model 2 supportaient des textures 2D et comme le filtrage de texture (bilinéaire et trilinéaire), le mip-mapping, et quelques autres. Au passage, les cartes graphiques de la Namco System 22 étaient développées en partenariat avec Eans & Sutherland, qui avait commencé à se diversifier dans le marché grand public. Les cartes graphiques de l'époque faisaient les calculs géométriques sur plusieurs processeurs, généralement des processeurs de type DSP (des processeurs spécialisés dans le traitement de signal). Par exemple, la Namco System 2 utilisait 4 DSP de marque Texas Instruments TMS320C25, cadencés à 24,576 MHz. La carte d'arcade Sega Model 1 utilisait quant à elle un DSP spécialisé dans les calculs géométriques. Par la suite, les bornes d'arcade ont réutilisé le hardware des PC et autres consoles de jeux. ==La 3D sur les consoles de quatrième/cinquième génération== Les consoles avant la quatrième génération de console étaient des consoles purement 2D, sans circuits d'accélération 3D. Leur carte graphique était un simple VDC 2D, plus ou moins performant selon la console. Les premières consoles de jeu capables de rendu 3D par elles-mêmes sont les consoles dites de 5ème génération. Il y a diverses manières de classer les consoles en générations, la plus commune place la 3D à la 5ème génération, mais détailler ces controverses quant à ce classement nous amènerait trop loin. Les consoles de génération avaient une architecture assez différente des systèmes antérieurs. Les systèmes SGI et assimilés pouvaient se permettre de couter assez cher, d'utiliser beaucoup de circuits, de prendre beaucoup de place. Les bornes d'arcade sont aussi dans ce cas. Aussi, il n'était pas rare que les cartes 3D de l'époque tiennent sur plusieurs cartes électroniques séparées. Mais une console ne peut pas se permettre ce genre de folies. Aussi, les cartes 3D des consoles de l'époque tenaient dans un seul circuit intégré, comme il est d'usage de nos jours. La conséquence est que certains circuits étaient fortement simplifiés, sur les consoles de cinquième génération. Et cela a impacté l'architecture interne des GPU des consoles. Les systèmes SGI avaient plusieurs processeurs pour calculer la géométrie, couplés à plusieurs unités non-programmables pour les pixels/textures. Les cartes 3D des consoles gardaient cette organisation : processeurs pour la géométrie, circuits fixes pour le reste. Mais elles se débrouillaient souvent avec un seul processeur, voire aucun ! Dans ce dernier cas, la géométrie était calculée sur le processeur principal, le CPU. Les unités pour les pixels étaient aussi moins nombreuses, mais il y en avait plusieurs, pour profiter de l'amplification des pixels. : Les cartes 3D des consoles de jeu utilisaient le placage de texture inverse, avec quelques exceptions qui utilisaient le placage de texture direct. ===Le rendu 3D sur les consoles de quatrième génération : la SNES=== Plus haut, j'ai dit que les consoles de quatrième génération n'avaient pas de carte accélératrice 3D. Pourtant, elles ont connus quelques jeux en vraie 3D. La raison à cela est que la 3D était calculée par un GPU placé dans les cartouches du jeu ! Par exemple, les cartouches de Starfox et de Super Mario 2 contenaient un coprocesseur Super FX, qui gérait des calculs de rendu 2D/3D. En tout, il y a environ 16 coprocesseurs pour la SNES et on en trouve facilement la liste sur le net. La console était conçue pour, des pins sur les ports cartouches étaient prévues pour des fonctionnalités de cartouche annexes, dont ces coprocesseurs. Ces pins connectaient le coprocesseur au bus des entrées-sorties. Les coprocesseurs des cartouches de NES avaient souvent de la mémoire rien que pour eux, qui était intégrée dans la cartouche. Ceci étant dit, passons aux consoles de cinquième génération. ===La Nintendo 64 : un GPU avancé=== La Nintendo 64 avait le GPU le plus complexe comparé aux autres consoles, et dépassait même les cartes graphiques des PC. Il faut dire que son GPU a été conçu avec l'aide de l'entreprise SGI, dont on a vu les systèmes graphiques plus haut. Le GPU de la N64 incorporait une unité pour les calculs géométriques, un circuit de rasterisation, une unité de textures et un ROP final pour les calculs de transparence/brouillard/antialiasing, ainsi qu'un circuit pour gérer la profondeur des pixels. En somme, tout le pipeline graphique était implémenté dans le GPU de la Nintendo 64, chose très en avance sur son temps, comparé au PC ou aux autres consoles ! Le GPU est construit autour d'un processeur dédié aux calculs géométriques, le ''Reality Signal Processor'' (RSP), autour duquel on a ajouté des circuits pour le reste du pipeline graphique. L'unité de calcul géométrique est un processeur MIPS R4000, un processeur assez courant à l'époque, auquel on avait retiré quelques fonctionnalités inutiles pour le rendu 3D. Il était couplé à 4 KB de mémoire vidéo, ainsi qu'à 4 KB de mémoire ROM. Le reste du GPU était réalisé avec des circuits fixes. Un point intéressant est que le programme exécuté par le RSP pouvait être programmé ! Le RSP gérait déjà des espèces de proto-shaders, qui étaient appelés des ''[https://ultra64.ca/files/documentation/online-manuals/functions_reference_manual_2.0i/ucode/microcode.html micro-codes]'' dans la documentation de l'époque. La ROM associée au RSP mémorise cinq à sept programmes différents, aux fonctionnalités différentes. * Les microcodes gspFast3D et gspF3DNoN, implémentent un rendu 3D normal, avec des options de ''clipping'' différentes entre les deux. * Le microcode gspTurbo3D fait la même chose, mais avec moins de fonctionnalités et avec une précision réduite. Il ne gère pas le ''clipping'', l'éclairage par pixel, la correction de perspective, l'antialiasing et quelques autres fonctionnalités. Il gère cependant l'éclairage de Gouraud. Il utilise une ''display list'' simplifiée comparé aux deux microcodes précédents. * Le microcode gspZ-Sort effectue une pré-passe z, à savoir qu'il calcule le tampon de profondeur final de la scène 3D, sans rendre l'image. Cela sert à faire une élimination des pixels cachés parfaite, en logiciel. On calcule le tampon de profondeur pour déterminer quels pixels sont visibles, puis une seconde passe rend l'image en, rejetant les pixels non-visibles. * Le microcode gspSprite2D implémente un rendu 2D émulé : les sprites et arrière-plan sont des rectangles texturés. Le microcode gspS2DEX fait la même chose, mais sert à émuler le rendu de la SNES plus qu'autre chose. * Le microcode gspLine3D ne gére que des lignes, pas de triangles. Il sert pour du rendu en fil de fer. Ils géraient le rendu 3D de manière différente et avec une gestion des ressources différentes. Très peu de studios de jeu vidéo ont développé leur propre microcodes N64, car la documentation était mal faite, que Nintendo ne fournissait pas de support officiel pour cela, que les outils de développement ne permettaient pas de faire cela proprement et efficacement. ===La Playstation 1=== Sur la Playstation 1 le calcul de la géométrie était réalisé par le processeur, la carte graphique gérait tout le reste. Et la carte graphique était un circuit fixe spécialisé dans la rasterisation et le placage de textures. Elle utilisait, comme la Nintendo 64, le placage de texture inverse, qui est apparu ensuite sur les cartes graphiques. ===La 3DO et la Sega Saturn=== La Sega Saturn et la 3DO étaient les deux seules consoles à utiliser le rendu direct. La géométrie était calculée sur le processeur, même si les consoles utilisaient parfois un CPU dédié au calcul de la géométrie. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. La Sega Saturn incorpore trois processeurs et deux GPU. Les deux GPUs sont nommés le VDP1 et le VDP2. Le VDP1 s'occupe des textures et des sprites, le VDP2 s'occupe uniquement de l'arrière-plan et incorpore un VDC tout ce qu'il y a de plus simple. Ils ne gèrent pas du tout la géométrie, qui est calculée par les trois processeurs. Le troisième processeur, la Saturn Control Unit, est un processeur de type DSP, à savoir un processeur spécialisé dans le traitement de signal. Il est utilisé presque exclusivement pour accélérer les calculs géométriques. Il avait sa propre mémoire RAM dédiée, 32 KB de SRAM, soit une mémoire locale très rapide. Les transferts entre cette RAM et le reste de l'ordinateur était géré par un contrôleur DMA intégré dans le DSP. En somme, il s'agit d'une sorte de processeur spécialisé dans la géométrie, une sorte d'unité géométrique programmable. Mais la géométrie n'était pas forcément calculée que sur ce DSP, mais pouvait être prise en charge par les 3 CPU. ==L'historique des cartes graphiques des PC, avant l'arrivée de Direct X et Open Gl== Sur PC, l'évolution des cartes graphiques a eu du retard par rapport aux consoles. Les PC sont en effet des machines multi-usage, pour lesquelles le jeu vidéo était un cas d'utilisation parmi tant d'autres. Et les consoles étaient la plateforme principale pour jouer à des jeux vidéo, le jeu vidéo PC étant plus marginal. Mais cela ne veut pas dire que le jeu PC n'existait pas, loin de là ! Un problème pour les jeux PC était que l'écosystème des PC était aussi fragmenté en plusieurs machines différentes : machines Apple 1 et 2, ordinateurs Commdore et Amiga, IBM PC et dérivés, etc. Aussi, programmer des jeux PC n'était pas mince affaire, car les problèmes de compatibilité étaient légion. C'est seulement quand la plateforme x86 des IBM PC s'est démocratisée que l'informatique grand public s'est standardisée, réduisant fortement les problèmes de compatibilité. Mais cela n'a pas suffit, il a aussi fallu que les API 3D naissent. Les API 3D comme Direct X et Open GL sont absolument cruciales pour garantir la compatibilité entre plusieurs ordinateurs aux cartes graphiques différentes. Aussi, l'évolution des cartes graphiques pour PC s'est faite main dans la main avec l'évolution des API 3D. Les fonctionnalités des cartes graphiques ont évolué dans le temps, en suivant les évolutions des API 3D. Du moins dans les grandes lignes, car il est arrivé plusieurs fois que des fonctionnalités naissent sur les cartes graphiques, pour que les fabricants forcent la main de Microsoft ou d'Open GL pour les intégrer de force dans les API 3D. Passons. ===L'introduction des premiers jeux 3D : Quake et les drivers miniGL=== L'API OpenGL est née de la main de SGI, encore eux ! SGI avait créé l'API Iris GL pour ses stations de travail Iris Graphics. Iris GL a ensuite été libéré et est devenu le standard Open GL. Open GL existait déjà avant l'apparition des cartes accélératrices 3D. Il y a avait donc déjà un terreau que les programmeurs graphiques pouvaient utiliser. Mais Open GL était surtout utilisé pour des applications industrielles, médicales (imagerie), graphiques ou militaires, pas pour le jeu vidéo. Mais cela changea avec la sortie du jeu Quake, d'IdSoftware, en 1996. Quake pouvait fonctionner en rendu logiciel, mais le programmeur responsable du moteur 3D (le célébre John Carmack) ajouta une version OpenGL du jeu. Il faut dire que le jeu était programmé sur une station de travail compatible avec OpenGL, même si aucune carte accélératrice de l'époque ne supportait OpenGL. C'était là un choix qui se révéla visionnaire. En théorie, le rendu par OpenGL aurait dû se faire intégralement en logiciel, sauf sur quelques rares stations de travail adaptées. Mais les premières cartes graphiques étaient déjà dans les starting blocks. La toute première carte 3D pour PC est la '''Rendition Vérité V1000''', sortie en Septembre 1995, soit quelques mois avant l'arrivée de la Nintendo 64. La Rendition Vérité V1000 contenait un processeur MIPS cadencé à 25 MHz, 4 mébioctets de RAM, une ROM pour le BIOS, et un RAMDAC, rien de plus. C'était un vrai ordinateur complètement programmable de bout en bout, sans aucun circuit fixe. Les programmeurs ne pouvaient cependant pas utiliser cette programmabilité avec des ''shaders'', mais elle permettait à Rendition d'implémenter n'importe quelle API 3D, que ce soit OpenGL, DirectX ou même sa son API propriétaire. La Rendition Vérité avait de bonnes performances pour ce qui est de la géométrie, mais pas pour le reste. Réaliser la rastérisation et le placage de texture en logiciel n'est pas efficace, pareil pour les opérations de fin de pipeline comme l'antialiasing. Le manque d'unités fixes très rapides pour la rastérisation, le placage de texture ou les opérations de fin de pipeline était clairement un gros défaut. Mais la Rendition Vérité était un cas à part, une exception dans le paysage des cartes 3D de l'époque, qui ne faisait rien comme les autres. Les autres cartes graphiques, sorties peu après, étaient les Voodoo de 3dfx, les Riva TNT de NVIDIA, les Rage/3D d'ATI, la Virge/3D de S3, et la Matrox Mystique. Elles avaient choisit le compromis inverse de la Rendition Vérité V1000 : de bonnes performances pour le placage de textures et la rastérization, mais pas pour les calculs géométriques. Pour rappel, les systèmes professionnels et les consoles avaient des processeurs pour la géométrie, et des circuits fixes pour le reste. Les cartes graphiques de PC se passaient des processeurs pour la géométrie, les calculs géométriques étaient réalisés par le CPU. Les toutes premières cartes 3D pour PC contenaient seulement des circuits pour gérer les textures et des ROPs. Elle géraient le ''z-buffer'' en mémoire vidéo, ainsi que des effets de brouillard. Il n'y avait même pas de circuit pour la rastérisation, qui était faite en logiciel, avec les calculs géométriques. [[File:Architecture de base d'une carte 3D - 2.png|centre|vignette|upright=1.5|Carte 3D sans rasterization matérielle.]] Les cartes suivantes ajoutèrent une gestion des étapes de ''rasterization'' directement en matériel. Les cartes ATI rage 2, les Invention de chez Rendition, et d'autres cartes graphiques supportaient la rasterisation en hardware. [[File:Architecture de base d'une carte 3D - 3.png|centre|vignette|upright=1.5|Carte 3D avec gestion de la géométrie.]] Pour exploiter les unités de texture et le circuit de rastérisation, OpenGL et Direct 3D étaient partiellement implémentées en logiciel, car les cartes graphiques ne supportaient pas toutes les fonctionnalités de l'API. C'était l'époque du miniGL, des implémentations partielles d'OpenGL, fournies par les fabricants de cartes 3D, implémentées dans les pilotes de périphériques de ces dernières. Les fonctionnalités d'OpenGL implémentées dans ces pilotes étaient presque toutes exécutées en matériel, par la carte graphique. Avec l'évolution du matériel, les pilotes de périphériques devinrent de plus en plus complets, au point de devenir des implémentations totales d'OpenGL. Mais au-delà d'OpenGL, chaque fabricant de carte graphique avait sa propre API propriétaire, qui était gérée par leurs pilotes de périphériques (''drivers''). Par exemple, les premières cartes graphiques de 3dfx interactive, les fameuses voodoo, disposaient de leur propre API graphique, l'API Glide. Elle facilitait la gestion de la géométrie et des textures, ce qui collait bien avec l'architecture de ces cartes 3D. Mais ces API propriétaires tombèrent rapidement en désuétude avec l'évolution de DirectX et d'OpenGL. Direct X était une API dans l'ombre d'Open GL. La première version de Direct X qui supportait la 3D était DirectX 2.0 (juin 2, 1996), suivie rapidement par DirectX 3.0 (septembre 1996). Elles dataient d'avant le jeu Quake, et elles étaient très éloignées du hardware des premières cartes graphiques. Elles utilisaient un système d'''execute buffer'' pour communiquer avec la carte graphique, Microsoft espérait que le matériel 3D implémenterait ce genre de système. Ce qui ne fu pas le cas. Direct X 4.0 a été abandonné en cours de développement pour laisser à une version 5.0 assez semblable à la 2.0/3.0. Le mode de rendu laissait de côté les ''execute buffer'' pour coller un peu plus au hardware de l'époque. Mais rien de vraiment probant comparé à Open GL. Même Windows utilisait Open GL au lieu de Direct X maison... C'est avec Direct X 6.0 que Direct X est entré dans la cours des grands. Il gérait la plupart des technologies supportées par les cartes graphiques de l'époque. ===Le ''multi-texturing'' de l'époque Direct X 6.0 : combiner plusieurs textures=== Une technologie très importante standardisée par Dirext X 6 est la technique du '''''multi-texturing'''''. Avec ce qu'on a dit dans le chapitre précédent, vous pensez sans doute qu'il n'y a qu'une seule texture par objet, qui est plaquée sur sa surface. Mais divers effet graphiques demandent d'ajouter des textures par dessus d'autres textures. En général, elles servent pour ajouter des détails, du relief, sur une surface pré-existante. Un exemple intéressant vient des jeux de tir : ajouter des impacts de balles sur les murs. Pour cela, on plaque une texture d'impact de balle sur le mur, à la position du tir. Il s'agit là d'un exemple de '''''decals''''', des petites textures ajoutées sur les murs ou le sol, afin de simuler de la poussière, des impacts de balle, des craquelures, des fissures, des trous, etc. Les textures en question sont de petite taille et se superposent à une texture existante, plus grande. Rendre des ''decals'' demande de pouvoir superposer deux textures. Direct X 6.0 supportait l'application de plusieurs textures directement dans le matériel. La carte graphique devait être capable d'accéder à deux textures en même temps, ou du moins faire semblant que. Pour cela, elle doublaient les unités de texture et adaptaient les connexions entre unités de texture et mémoire vidéo. La mémoire vidéo devait être capable de gérer plusieurs accès mémoire en même temps et devait alors avoir un débit binaire élevé. [[File:Multitexturing.png|centre|vignette|upright=2|Multitexturing]] La carte graphique devait aussi gérer de quoi combiner deux textures entre elles. Par exemple, pour revenir sur l'exemple d'une texture d'impact de balle, il faut que la texture d'impact recouvre totalement la texture du mur. Dans ce cas, la combinaison est simple : la première texture remplace l'ancienne, là où elle est appliquée. Mais les cartes graphiques ont ajouté d'autres combinaisons possibles, par exemple additionner les deux textures entre elle, faire une moyenne des texels, etc. Les opérations pour combiner les textures était le fait de circuits appelés des '''''combiners'''''. Concrètement, les ''combiners'' sont de simples unités de calcul. Les ''conbiners'' ont beaucoup évolués dans le temps, mais les premières implémentation se limitaient à quelques opérations simples : addition, multiplication, superposition, interpolation. L'opération effectuer était envoyée au ''conbiner'' sur une entrée dédiée. [[File:Multitexturing avec combiners.png|centre|vignette|upright=2|Multitexturing avec combiners]] S'il y avait eu un seul ''conbiner'', le circuit de ''multitexturing'' aurait été simplement configurable. Mais dans la réalité, les premières cartes utilisant du ''multi-texturing'' utilisaient plusieurs ''combiners'' placés les uns à la suite des autres. L'implémentation des ''combiners'' retenue par Open Gl, et par le hardware des cartes graphiques, était la suivante. Les ''combiners'' étaient placés en série, l'un à la suite de l'autre, chacun combinant le résultat de l'étage précédent avec une texture. Le premier ''combiner'' gérait l'éclairage par sommet, afin de conserver un minimum de rétrocompatibilité. [[File:Texture combiners Open GL.png|centre|vignette|upright=2|Texture combiners Open GL]] Voici les opérations supportées par les ''combiners'' d'Open GL. Ils prennent en entrée le résultat de l'étage précédent et le combinent avec une texture lue depuis l'unité de texture. {|class="wikitable" |+ Opérations supportées par les ''combiners'' d'Open GL |- ! Replace | colspan="2" | Pixel provenant de l'unité de texture |- ! Addition | colspan="2" | Additionne l'entrée au texel lu. |- ! Modulate | colspan="2" | Multiplie l'entrée avec le texel lu |- ! Mélange (''blending'') | Moyenne pondérée des deux entrées, pondérée par la composante de transparence || La couleur de transparence du texel lu et de l'entrée sont multipliées. |- ! Decals | Moyenne pondérée des deux entrées, pondérée par la composante de transparence. || La transparence du résultat est celle de l'entrée. |} Il faut noter qu'un dernier étage de ''combiners'' s'occupait d'ajouter la couleur spéculaire et les effets de brouillards. Il était à part des autres et n'était pas configurable, c'était un étage fixe, qui était toujours présent, peu importe le nombre de textures utilisé. Il était parfois appelé le '''''combiner'' final''', terme que nous réutiliserons par la suite. Mine de rien, cela a rendu les cartes graphiques partiellement programmables. Le fait qu'il y ait des opérations enchainées à la suite, opérations qu'on peut choisir librement, suffit à créer une sorte de mini-programme qui décide comment mélanger plusieurs textures. Mais il y avait une limitation de taille : le fait que les données soient transmises d'un étage à l'autre, sans détours possibles. Par exemple, le troisième étage ne pouvait avoir comme seule opérande le résultat du second étage, mais ne pouvait pas utiliser celui du premier étage. Il n'y avait pas de registres pour stocker ce qui sortait de la rastérisation, ni pour mémoriser temporairement les texels lus. ===Le ''Transform & Lighting'' matériel de Direct X 7.0=== [[File:Architecture de base d'une carte 3D - 4.png|vignette|upright=1.5|Carte 3D avec gestion de la géométrie.]] La première carte graphique pour PC capable de gérer la géométrie en hardware fût la Geforce 256, la toute première Geforce. Son unité de gestion de la géométrie n'est autre que la bien connue '''unité T&L''' (''Transform And Lighting''). Elle implémentait des algorithmes d'éclairage de la scène 3D assez simples, comme un éclairage de Gouraud, qui étaient directement câblés dans ses circuits. Mais contrairement à la Nintendo 64 et aux bornes d'arcade, elle implémentait le tout, non pas avec un processeur classique, mais avec des circuits fixes. Avec Direct X 7.0 et Open GL 1.0, l'éclairage était en théorie limité à de l'éclairage par sommet, l'éclairage par pixel n'était pas implémentable en hardware. Les cartes graphiques ont tenté d'implémenter l'éclairage par pixel, mais cela n'est pas allé au-delà du support de quelques techniques de ''bump-mapping'' très limitées. Par exemple, Direct X 6.0 implémentait une forme limitée de ''bump-mapping'', guère plus. Un autre problème est qu'il a beaucoup d'algorithmes d'éclairages différents, aux résultats visuels différents, bien au-delà des algorithmes d'éclairage plat, de Gouraud et de Phong. Et les unités de T&L étaient souvent en retard sur les algorithmes logiciels. Les programmeurs avaient le choix entre programmer les algorithmes d’éclairage qu'ils voulaient et les exécuter en logiciel, ou utiliser ceux de l'unité de T&L. Ils choisissaient souvent la première option. Par exemple, Quake 3 Arena et Unreal Tournament n'utilisaient pas les capacités d'éclairage géométrique et préféraient utiliser leurs calculs d'éclairage logiciel fait maison. Cependant, le hardware dépassait les capacités des API et avait déjà commencé à ajouter des capacités de programmation liées au ''multi-texturing''. Les cartes graphiques de l'époque, surtout chez NVIDIA, implémentaient un système de '''''register combiners''''', une forme améliorée de ''texture combiners'', qui permettait de faire une forme limitée d'éclairage par pixel, notamment du vrai ''bump-mampping'', voire du ''normal-mapping''. Mais ce n'était pas totalement supporté par les API 3D de l'époque. Les ''registers combiners'' sont des ''texture combiners'' mais dans lesquels ont aurait retiré la stricte organisation en série. Il y a toujours plusieurs étages à la suite, qui peuvent exécuter chacun une opération, mais tous les étages ont maintenant accès à toutes les textures lues et à tout ce qui sort de la rastérisation, pas seulement au résultat de l'étape précédente. Pour cela, on ajoute des registres pour mémoriser ce qui sort des unités de texture, et pour ce qui sort de la rastérisation. De plus, on ajoute des registres temporaires pour mémoriser les résultats de chaque ''combiner'', de chaque étage. Il faut cependant signaler qu'il existe un ''combiner'' final, séparé des étages qui effectuent des opérations proprement dits. Il s'agit de l'étage qui applique la couleur spéculaire et les effets de brouillards. Il ne peut être utilisé qu'à la toute fin du traitement, en tant que dernier étage, on ne peut pas mettre d'opérations après lui. Sa sortie est directement connectée aux ROPs, pas à des registres. Il faut donc faire la distinction entre les '''''combiners'' généraux''' qui effectuent une opération et mémorisent le résultat dans des registres, et le ''combiner'' final qui envoie le résultat aux ROPs. L'implémentation des ''register combiners'' utilisait un processeur spécialisés dans les traitements sur des pixels, une sorte de proto-processeur de ''shader''. Le processeur supportait des opérations assez complexes : multiplication, produit scalaire, additions. Il s'agissait d'un processeur de type VLIW, qui sera décrit dans quelques chapitres. Mais ce processeur avait des programmes très courts. Les premières cartes NVIDIA, comme les cartes TNT pouvaient exécuter deux opérations à la suite, suivie par l'application de la couleurs spéculaire et du brouillard. En somme, elles étaient limitées à un ''shader'' à deux/trois opérations, mais c'était un début. Le nombre d'opérations consécutives est rapidement passé à 8 sur la Geforce 3. ===L'arrivée des ''shaders'' avec Direct X 8.0=== [[File:Architecture de la Geforce 3.png|vignette|upright=1.5|Architecture de la Geforce 3]] Les ''register combiners'' était un premier pas vers un éclairage programmable. Paradoxalement, l'évolution suivante s'est faite non pas dans l'unité de rastérisation/texture, mais dans l'unité de traitement de la géométrie. La Geforce 3 a remplacé l'unité de T&L par un processeur capable d'exécuter des programmes. Les programmes en question complétaient l'unité de T&L, afin de pouvoir rajouter des techniques d'éclairage plus complexes. Le tout a permis aussi d'ajouter des animations, des effets de fourrures, des ombres par ''shadow volume'', des systèmes de particule évolués, et bien d'autres. À partir de la Geforce 3 de Nvidia, les cartes graphiques sont devenues capables d'exécuter des programmes appelés '''''shaders'''''. Le terme ''shader'' vient de ''shading'' : ombrage en anglais. Grace aux ''shaders'', l'éclairage est devenu programmable, il n'est plus géré par des unités d'éclairage fixes mais été laissé à la créativité des programmeurs. Les programmeurs ne sont plus vraiment limités par les algorithmes d'éclairage implémentés dans les cartes graphiques, mais peuvent implémenter les algorithmes d'éclairage qu'ils veulent et peuvent le faire exécuter directement sur la carte graphique. Les ''shaders'' sont classifiés suivant les données qu'ils manipulent : '''''pixel shader''''' pour ceux qui manipulent des pixels, '''''vertex shaders''''' pour ceux qui manipulent des sommets. Les premiers sont utilisés pour implémenter l'éclairage par pixel, les autres pour gérer tout ce qui a trait à la géométrie, pas seulement l'éclairage par sommets. Direct X 8.0 avait un standard pour les shaders, appelé ''shaders 1.0'', qui correspondait parfaitement à ce dont était capable la Geforce 3. Il standardisait les ''vertex shaders'' de la Geforce 3, mais il a aussi renommé les ''register combiners'' comme étant des ''pixel shaders'' version 1.0. Les ''register combiners'' n'ont pas évolués depuis la Geforce 256, si ce n'est que les programmes sont passés de deux opérations successives à 8, et qu'il y avait possibilité de lire 4 textures en ''multitexturing''. A l'opposé, le processeur de ''vertex shader'' de la Geforce 3 était capable d'exécuter des programmes de 128 opérations consécutives et avait 258 registres différents ! Des ''pixels shaders'' plus évolués sont arrivés avec l'ATI Radeon 8500 et ses dérivés. Elle incorporait la technologie ''SMARTSHADER'' qui remplacait les ''registers combiners'' par un processeur de ''shader'' un peu limité. Un point est que le processeur acceptait de calculer des adresses de texture dans le ''pixel shader''. Avant, les adresses des texels à lire étaient fournis par l'unité de rastérisation et basta. L'avantage est que certains effets graphiques étaient devenus possibles : du ''bump-mapping'' avancé, des textures procédurales, de l'éclairage par pixel anisotrope, du éclairage de Phong réel, etc. Avec la Radeon 8500, le ''pixel shader'' pouvait calculer des adresses, et lire les texels associés à ces adresses calculées. Les ''pixel shaders'' pouvaient lire 6 textures, faire 8 opérations sur les texels lus, puis lire 6 textures avec les adresses calculées à l'étape précédente, et refaire 8 opérations. Quelque chose de limité, donc, mais déjà plus pratique. Les ''pixel shaders'' de ce type ont été standardisé dans Direct X 8.1, sous le nom de ''pixel shaders 1.4''. Encore une fois, le hardware a forcé l'intégration dans une API 3D. ===Les ''shaders'' de Direct X 9.0 : de vrais ''pixel shaders''=== Avec Direct X 9.0, les ''shaders'' sont devenus de vrais programmes, sans les limitations des ''shaders'' précédents. Les ''pixels shaders'' sont passés à la version 2.0, idem pour les ''vertex shaders''. Concrètement, ils ont des fonctionnalités bien supérieures à celles des ''registers combiners''. Les ''shaders'' pouvaient exécuter une suite d'opérations arbitraire, dans le sens où elle n'était pas structurée avec tel type d'opération au début, suivie par un accès aux textures, etc. On pouvait mettre n'importe quelle opération dans n'importe quel ordre. De plus, les ''shaders'' ne sont plus écrit en assembleur comme c'était le cas avant. Ils sont dorénavant écrits dans un langage de haut-niveau, le HLSL pour les shaders Direct X et le GLSL pour les shaders Open Gl. Les ''shaders'' sont ensuite traduit (compilés) en instructions machines compréhensibles par la carte graphique. Au début, ces langages et la carte graphique supportaient uniquement des opérations simples. Mais au fil du temps, les spécifications de ces langages sont devenues de plus en plus riches à chaque version de Direct X ou d'Open Gl, et le matériel en a fait autant. Le matériel s'est alors adapté, en incorporant un véritable processeur pour les ''pixel shaders''. Les ''pixel shaders'' sont maintenant exécutés par un processeur de ''shader'' dédié, aux fonctionnalités bien supérieures à celles des ''registers combiners''. Le processeur de ''pixel shader'' incorpore l'unité de texture en sont sein, les deux sont fusionnés. La raison à cela sera expliqué dans la suite du chapitre. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=1.5|Carte 3D avec pixels et vertex shaders non-unifiés.]] ===L'après Direct X 9.0=== Avant Direct X 10, les processeurs de ''shaders'' ne géraient pas exactement les mêmes opérations pour les processeurs de ''vertex shader'' et de ''pixel shader''. Les processeurs de ''vertex shader'' et de ''pixel shader''étaient séparés. Depuis DirectX 10, ce n'est plus le cas : le jeu d'instructions a été unifié entre les vertex shaders et les pixels shaders, ce qui fait qu'il n'y a plus de distinction entre processeurs de vertex shaders et de pixels shaders, chaque processeur pouvant traiter indifféremment l'un ou l'autre. [[File:Architecture de base d'une carte 3D - 6.png|centre|vignette|upright=1.5|Architecture de la GeForce 6800.]] Avec Direct X 10, de nombreux autres ''shaders'' sont apparus. Les plus liés au rendu 3D sont les '''''geometry shader''''' pour ceux qui manipulent des triangles, de ''hull shaders'' et de ''domain shaders'' pour la tesselation. De plus, les cartes graphiques modernes sont capables d’exécuter des programmes informatiques qui n'ont aucun lien avec le rendu 3D, mais sont exécutés par la carte graphique comme le ferait un processeur d'ordinateur normal. De tels ''shaders'' sans lien avec le rendu 3D sont appelés des ''compute shader''. ==Les cartes graphiques d'aujourd'hui== Les circuits d'un GPU ont beaucoup évolué depuis l'introduction des ''shaders'', pour devenir de plus en plus programmables. Mais à côté des processeurs de ''shaders'', il reste quelques circuits non-programmables appelés des circuits fixes. La rastérisation, le placage de texture, l'élimination des pixels cachés et le mélange ''alpha'' sont gérés par des circuits fixes. [[File:3D-Pipeline.svg|centre|vignette|upright=3.0|Pipeline 3D : ce qui est programmable et ce qui ne l'est pas dans une carte graphique moderne.]] Mais pourquoi ne pas tout rendre programmable ? Ou au contraire, utiliser seulement des circuits fixes ? La réponse rapide est qu'il s'agit d'un compromis entre flexibilité et performance qui permet d'avoir le meilleur des deux mondes. Mais ce compromis a fortement évolué dans le temps, comme on va le voir plus bas. Rendre l'éclairage programmable permet d'implémenter facilement un grand nombre d'effets graphiques sans avoir à les implémenter en hardware. Avant les ''shaders'', les effets graphiques derniers cri n'étaient disponibles que sur les derniers modèles de carte graphique. Avec des ''vertex/pixel shaders'', ce genre de défaut est passé à la trappe. Si un nouvel algorithme de rendu graphique est inventé, il peut être utilisé dès le lendemain sur toutes les cartes graphiques modernes. De plus, implémenter beaucoup d'algorithmes d'éclairage différents avec des circuits fixes a un cout en termes de transistors, alors qu'utiliser des circuits programmable a un cout en hardware plus limité. Tout cela est à l'exact opposé de ce qu'on a avec les autres circuits, comme les circuits pour la rastérisation ou le placage de texture. Il n'y a pas 36 façons de rastériser une scène 3D et la flexibilité n'est pas un besoin important pour cette opération, alors que les performances sont cruciales. Même chose pour le placage/filtrage de textures. En conséquences, les unités de rastérisation, de texture, et les ROPs sont toutes implémentées en matériel. Faire ainsi permet de gagner en performance sans que cela ait le moindre impact pour le programmeur. Reste à expliquer dans le détail pourquoi. ===Les unités de texture sont intégrées aux processeurs de shaders=== Avec l'arrivée des processeurs de shaders, les unités de texture ont été intégrées dans les processeurs de shaders eux-mêmes. C'est la seule unité fixe qui a subit ce traitement, et il est intéressant de comprendre pourquoi. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=2|Architecture de base d'une carte 3D.]] Pour cela, il faut faire un rappel sur ce qu'il y a dans un processeur. Un processeur contient globalement quatre circuits : * une unité de calcul qui fait des calculs ; * des registres pour stocker les opérandes et résultats des calculs ; * une unité de communication avec la mémoire ; * et un séquenceur, un circuit de contrôle qui commande les autres. L'unité de communication avec la mémoire sert à lire ou écrire des données, à les transférer de la RAM vers les registres, ou l'inverse. Lire une donnée demande d'envoyer son adresse à la RAM, qui répond en envoyant la donnée lue. Elle est donc toute indiquée pour lire une texture : lire une texture n'est qu'un cas particulier de lecture de données. Les texels à lire sont à une adresse précise, la RAM répond à la lecture avec le texel demandé. Il est donc possible d'utiliser l'unité de communication avec la mémoire comme si c'était une unité de texture. Cependant, les textures ne sont pas utilisées comme telles de nos jours. Le rendu 3D moderne utilise des techniques dites de filtrage de texture, qui permettent d'améliorer la qualité du rendu des textures. Sans ce filtrage de texture, les textures appliquées naïvement donnent un résultat assez pixelisé et assez moche, pour des raisons assez techniques. Le filtrage élimine ces artefacts, en utilisant une forme d'''antialiasing'' interne aux textures, le fameux filtrage de texture. Le filtrage de texture peut être réalisé en logiciel ou en matériel. Techniquement, il est possible de le faire dans un ''shader''. Le ''shader'' calcule les adresses des texels à lire, lit les texels, et effectue ensuite le filtrage avec des opérations de calcul. Mais ce n'est pas ce qui est fait, le filtrage de texture est toujours effectué directement en matériel. La raison est que le filtrage de texture est très simple à implémenter en hardware. Le filtrage bilinéaire ou trilinéaire demande juste des circuits d'interpolation et quelques registres, ce qui est trivial. Et la seconde raison est qu'il n'y a pas 36 façons de filtrer des textures : une carte graphique peut implémenter les algorithmes principaux existants en assez peu de circuits. Pour simplifier l'implémentation, les processeurs de ''shader'' modernes disposent d'une unité d'accès mémoire séparée de l'unité de texture. L'unité d'accès mémoire normale s'occupe des accès mémoire hors-textures, alors que l'unité mémoire s'occupe de lire les textures. L'unité de texture contient de quoi faire du filtrage de texture, mais aussi faire des calculs d'adresse spécialisées, intrinsèquement liés au format des textures, qu'on détaillera dans le chapitre sur les textures. En comparaison, les unités d'accès mémoire effectuent des calculs d'adresse plus basiques. Un dernier avantage est que l'unité de texture est reliée au cache de texture, alors que l'unité d'accès mémoire est relié au cache L1/L2. ===Les ROPs peuvent être implémentés dans le ''pixel shader''=== Les ROPs effectuent plusieurs opérations basiques, mais les deux plus importantes sont la gestion du tampon de profondeur et de la transparence. Par transparence, on veut parler du mélange ''alpha''. Pour la gestion du tampon de profondeur, on veut parler du ''z-test'', qui compare la profondeur de deux pixels/fragments. Il s'agit d'opérations simples : comparaison pour le ''z-test'', additions et multiplications pour le mélange ''alpha''. Les processeurs de shader savent faire des opérations. Il leur suffit de lire les données nécessaires dans le tampon de profondeur ou le ''framebuffer'' pour effectuer ces deux opérations. Il est donc possible d'émuler les ROPs dans les pixels shaders. En pratique, c'est assez rare. Les cartes graphiques pour PC ont des ROPs séparés des processeurs de ''shaders''. Par contre, quelques cartes graphiques destinées les smartphones et autres appareils mobiles font exception. Sur celles-ci, les unités de textures et les ROPs sont intégrés dans les processeurs de ''shaders'', aux côtés de l'unité d'accès mémoire LOAD/STORE. Il s'agit de cartes graphiques en rendu à tuiles. {{NavChapitre | book=Les cartes graphiques | prev=Les cartes graphiques : architecture de base | prevText=Les cartes graphiques : architecture de base | next=Les processeurs de shaders | nextText=Les processeurs de shaders }} {{autocat}} 9cyeeu5cvkfa75we1xdd51i3o728tnv 763506 763504 2026-04-11T20:01:25Z Mewtow 31375 /* Les ROPs peuvent être implémentés dans le pixel shader */ 763506 wikitext text/x-wiki Il est intéressant d'étudier le hardware des cartes graphiques en faisant un petit résumé de leur évolution dans le temps. En effet, leur hardware a fortement évolué dans le temps. Et il serait difficile à comprendre le hardware actuel sans parler du hardware d'antan. En effet, une carte graphique moderne est partiellement programmable. Certains circuits sont totalement programmables, d'autres non. Et pour comprendre pourquoi, il faut étudier comment ces circuits ont évolués. Le hardware des cartes graphiques a fortement évolué dans le temps, ce qui n'est pas une surprise. Les évolutions de la technologie, avec la miniaturisation des transistors et l'augmentation de leurs performances a permis aux cartes graphiques d'incorporer de plus en plus de circuits avec les années. Avant l'invention des cartes graphiques, toutes les étapes du pipeline graphique étaient réalisées par le processeur : il calculait l'image à afficher, et l’envoyait à une carte d'affichage 2D. Au fil du temps, de nombreux circuits furent ajoutés, afin de déporter un maximum de calculs vers la carte vidéo. Le rendu 3D moderne est basé sur le placage de texture inverse, avec des coordonnées de texture, une correction de perspective, etc. Mais les anciennes consoles et bornes d'arcade utilisaient le placage de texture direct. Et cela a impacté le hardware des consoles/PCs de l'époque. Avec le placage de texture direct, il était primordial de calculer la géométrie, mais la rasterisation était le fait de VDC améliorés. Aussi, les premières bornes d'arcade 3D et les consoles de 5ème génération disposaient processeurs pour calculer la géométrie et de circuits d'application de textures très particuliers. A l'inverse, les PC utilisaient un rendu inverse, totalement différent. Sur les PC, les premières cartes graphiques avaient un circuit de rastérisation et des unités de textures, mais pas de circuits géométriques. ==Les premières cartes graphiques== Dès les années 70-80, le rendu 3D était utilisé par de nombreuses entreprises industrielles : des applications de visualisation 3D étaient utilisées en architecture, des applications de conception assistée par ordinateur étaient déjà d'utilisation courante, sans compter les simulateurs de vol utilisés par l'armée et les instructeurs qui formaient les pilotes d'avion. Le rendu 3D était aussi étudié au niveau académique, la recherche en 3D était déjà florissante. Il existait même du matériel spécifiquement conçu pour le rendu graphique, mais celui-ci était spécifiquement dédié à des super-calculateurs ou des ''workstations'' (des sortes d'ancêtres des PC, très puissants pour l'époque, mais conçus uniquement pour les entreprises). ===Le début des années 80 : le rendu en fils de fer=== Le tout premier système de ce genre était le '''''Line Drawing System-1''''' de l'entreprise Evans & Sutherland, daté de 1969. Ce n'est ni plus ni moins que le toute premier circuit graphique séparé du processeur ayant existé. C'est en un sens la toute première carte graphique, le tout premier GPU. Il prenait la forme d'un périphérique qui se connectait à l'ordinateur d'un côté et était relié à l'écran de l'autre. Il était compatible avec un grand nombre d'ordinateurs et de processeurs existants. Il a été suivi par plusieurs successeurs, nommés ''Picture System 1, 2'' et le ''PS300 series''. [[File:Evans & Sutherland LDS-1 (1).jpg|vignette|Evans & Sutherland LDS-1 (1)]] Ils permettaient de faire du rendu en fil de fer, sans texture ni même sans polygones colorés. Un tel rendu était utile pour des applications assez limitées : architecture, dessin de molécules pour les entreprises pharmaceutique et certains centres de recherche, l'aérospatiale, etc. Ces cartes graphiques étaient utilisées de concert avec des écrans appelés '''écrans vectoriels''' (''vector display''). Pour simplifier, ils ressemblaient à des écrans CRT, sauf que le faisceau d'électron ne balayait pas l'écran ligne par ligne, mais traçait des lignes arbitraires à l'écran. On lui précisait deux points de coordonnées x1,y1 ; et x2,y2 ; puis l'écran tracait une ligne entre ces deux points. En général, la ligne tracée était maintenue pendant un long moment, entre plusieurs secondes et plusieurs minutes. L'intérieur du circuit était assez simple : un circuit de multiplication de matrice pour les calculs géométriques, un rastériser simplifié (le ''clipping diviser''), un circuit de tracé de lignes, et un processeur de contrôle pour commander les autres circuits. Le fait que ces trois circuits soient séparés permettait une implémentation en pipeline, où plusieurs portions de l'image pouvaient être calculées en même temps : pendant que l'une est dans l'unité géométrique, l'autre est dans le rastériseur et une troisième est en cours de tracé. [[File:Lds1blockdiagram05.svg|centre|vignette|upright=2|Architecture du LDS-1. Le processeur de contrôle n'est pas représenté.]] Le processeur de contrôle exécute un programme qui se charge de commander l'unité géométrique et les autres circuits. Le programme en question est fourni par le programmeur, le LDS-1 est donc totalement programmable. Il lit directement les données nécessaires pour le rendu dans la mémoire de l’ordinateur et le programme exécuté est lui aussi en mémoire principale. Il n'a pas de mémoire vidéo dédiée, il utilise la RAM de l'ordinateur principal. Le multiplieur de matrices est plus complexe qu'on pourrait s'y attendre. Il ne s'agit pas que d'un circuit arithmétique tout simple, mais d'un véritable processeur avec des registres et des instructions machine complexes. Il contient plusieurs registres, l'ensemble mémorisant 4 matrices de 16 nombres chacune (4 lignes de 4 colonnes). Un nombre est codé sur 18 bits. Les registres sont reliés à un ensemble de circuits arithmétiques, des additionneurs et des multiplieurs. Le circuit supporte des instructions de copie entre registres, pour copier une ligne d'une matrice à une autre, des instructions LOAD/STORE pour lire ou écrire dans la mémoire RAM, etc. Il supporte aussi des multiplications en 2D et 3D. Le ''clipping divider'' est un circuit assez complexe, contenant un processeur à accumulateur, une mémoire ROM pour le programme du processeur. Le programme exécuté par le processeur est un petit programme de 62 instructions, stocké dans la ROM. L'algorithme du ''clipping divider'' est décrite dans le papier de recherche "A clipping divider", écrit par Robert Sproull. Un détail assez intéressant est que le résultat en sortie de l'unité géométrique et du rastériseur peuvent être envoyés à l'ordinateur en parallèle du rendu. C'était très utile sur les anciens ordinateurs qui étaient connectés à plusieurs terminaux. Le LDS-1 calculait la géométrie et le rendu, et le tout pouvait petre envoyé à d'autres composants, comme des terminaux, une imprimante, etc. ===Les systèmes ultérieurs : rendu à triangles colorés et texturé=== Les systèmes précédents étaient très limités : ils calculaient la géométrie et n'avaient pas de ''framebuffer'', ni de tampon de profondeur, ni gestion de l'éclairage, ni quoique ce soit. De tels systèmes étaient donc des accélérateurs géométriques que de vrais systèmes graphiques complets, du fait de l'absence de ''framebuffer''. Ils étaient composés de processeurs spécialisés dans les calculs à virgule flottante, faisant des calculs géométriques, et éventuellement d'un processeur pour la rastérisation. La raison est que la RAM était très chère et que créer des circuits fixes étaient très chers et peu disponibles. Par contre, les processeurs à virgule flottante étaient peu chers et facile à trouver. Vers la fin des années 80, grâce à la baisse du prix de la RAM et la démocratisation des ASIC (des circuits fixes fait sur mesure), ajouter un ''framebuffer'' est est devenu possible. C'est alors que sont apparus les '''systèmes de rendu 3D de première génération'''. De tels systèmes ont permis d'implémenter le rendu à primitives colorées qu'on a vu il y a quelques chapitres, à savoir un rendu où les triangles sont coloriés avec une couleur unique. Les systèmes de première génération étaient simples : des processeurs pour le calcul de la géométrie, un circuit de rastérisation, une RAM pour le ''framebuffer'' et des ASIC servant de ROPs très simples. Il n'y avait pas d'élimination des pixels cachés, pas de textures, et encore moins d'éclairage par pixels. Le premier système de ce genre était le ''Shaded Picture System'', toujours par Evans & Sutherland. Il ne gérait pas la couleur et ne pouvait afficher que des images en noir et blanc, mais il gérait l'éclairage par sommet (''vertex lighting''). Il a rapidement été dépassé par les systèmes de l'entreprise ''Silicon Graphics Inc'' (SGI), ainsi que ceux de l'entreprise Apollo avec sa série Apollo DN. Les '''systèmes de seconde génération''' sont apparus vers la fin des années 80, et se distinguent des précédents par l'ajout un tampon de profondeur. Ils intègrent aussi des capacités d'éclairage par pixel, à savoir de l'éclairage plat, de Gouraud, voire de Phong ! Enfin, les '''systèmes de troisième génération''' ont acquis des capacités de placage de texture, que les systèmes précédents n'avaient pas. Ils ont aussi ajouté un support de l'antialiasing. Les systèmes SGI avec placage de texture ont déjà été abordé au chapitre précédent, dans la section sur les GPU en mode immédiat et à ''tile''. Aussi, nous ne reviendrons pas dessus. [[File:Evolution de l'architecture des premières cartes graphiques, dans les années 80-90.png|centre|vignette|upright=2.5|Evolution de l'architecture des premières cartes graphiques, dans les années 80-90]] Les systèmes de première, seconde et troisième génération avaient de nombreux points communs. En premier lieu, ils étaient fabriqués en connectant plusieurs cartes électroniques : une carte pour les calculs géométriques, une ou plusieurs cartes pour le reste du rendu graphique, une carte dédiée au VDC et avec un connecteur écran. Les transistors de l'époque n'étaient pas encore miniaturisés, ce qui fait que le système graphique ne pouvait pas tenir sur une seule carte électronique. Il n'y avait donc pas de carte graphique proprement dit, mais un équivalent éclaté sur plusieurs cartes électroniques. La carte pour la géométrie contenait typiquement une mémoire FIFO pour accumuler les commandes de rendu, un processeur de commande, et plusieurs processeurs géométriques. Les processeurs géométriques étaient parfois conçus sur mesure, comme l'a été le le ''Geometry Engine'' de SGI. Mais il est arrivé qu'ils utilisent des processeurs commerciaux comme le Weitek 3222, l'Intel i860, etc. Les processeurs pouvaient être placés en série ou en parallèle, comme expliqué dans le chapitre précédent. Le circuit de rastérisation était réalisé soit avec un processeur dédié, soit avec un circuit fixe, soit un mélange des deux. La rastérisation est en effet réalisée en plusieurs étapes, certaines peuvent être implémentées avec un processeur et d'autres avec des circuits fixes. Un point important est qu'à l'époque, le rendu n'utilisait pas que des triangles, mais des polygones en général. Ce n'est que par la suite que le rendu s'est focalisé sur les triangles et les ''quads'' (quadrilatères). Il arrivait que le système graphique gérait partiellement des polygones concaves, voire convexes. Sur les systèmes SGI, les calculs géométriques se faisaient avec des polygones, que la rastérisation découpait en triangles, le reste du rendu se faisait avec des triangles. Les stations de travail Apollo DN 10000VS découpaient les polygones en trapézoïdes orientés à l'horizontale, alignés avec des ''scanlines''. D'autres systèmes découpaient tout en triangle lors de l'étape géométrique ==Les précurseurs grand public : les bornes d'arcade== [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] L'accélération du rendu 3D sur les bornes d'arcade était déjà bien avancé dès les années 90. Les bornes d'arcade ont toujours été un segment haut de gamme de l'industrie du jeu vidéo, aussi ce n'est pas étonnant. Le prix d'une borne d'arcade dépassait facilement les 10 000 dollars pour les plus chères et une bonne partie du prix était celui du matériel informatique. Le matériel était donc très puissant et débordait de mémoire RAM comparé aux consoles de jeu et aux PC. La plupart des bornes d'arcade utilisaient du matériel standardisé entre plusieurs bornes. A l'intérieur d'une borne d'arcade se trouve une '''carte de borne d'arcade''' qui est une carte mère avec un ou plusieurs processeurs, de la RAM, une carte graphique, un VDC et pas mal d'autres matériels. La carte est reliée aux périphériques de la borne : joysticks, écran, pédales, le dispositif pour insérer les pièces afin de payer, le système sonore, etc. Le jeu utilisé pour la borne est placé dans une cartouche qui est insérée dans un connecteur spécialisé. Les cartes de bornes d'arcade étaient généralement assez complexes, elles avaient une grande taille et avaient plus de composants que les cartes mères de PC. Chaque carte contenait un grand nombre de chips pour la mémoire RAM et ROM, et il n'était pas rare d'avoir plusieurs processeurs sur une même carte. Et il n'était pas rare d'avoir trois à quatre cartes superposées dans une seule borne. Pour ceux qui veulent en savoir plus, Fabien Sanglard a publié gratuitement un livre sur le fonctionnement des cartes d'arcade CPS System, disponible via ce lien : [https://fabiensanglard.net/b/cpsb.pdf The book of CP System]. Les premières cartes graphiques des bornes d'arcade étaient des cartes graphiques 2D auxquelles on avait ajouté quelques fonctionnalités. Les sprites pouvaient être tournés, agrandit/réduits, ou déformés pour simuler de la perspective et faire de la fausse 3D. Par la suite, le vrai rendu 3D est apparu sur les bornes d'arcade. Dès 1988, la carte d'arcade Namco System 21 et Sega Model 1 géraient les calculs géométriques. Quelques années plus tard, les cartes graphiques se sont mises à supporter un éclairage de Gouraud et du placage de texture. Par exemple, le Namco System 22 et la Sega model 2 supportaient des textures 2D et comme le filtrage de texture (bilinéaire et trilinéaire), le mip-mapping, et quelques autres. Au passage, les cartes graphiques de la Namco System 22 étaient développées en partenariat avec Eans & Sutherland, qui avait commencé à se diversifier dans le marché grand public. Les cartes graphiques de l'époque faisaient les calculs géométriques sur plusieurs processeurs, généralement des processeurs de type DSP (des processeurs spécialisés dans le traitement de signal). Par exemple, la Namco System 2 utilisait 4 DSP de marque Texas Instruments TMS320C25, cadencés à 24,576 MHz. La carte d'arcade Sega Model 1 utilisait quant à elle un DSP spécialisé dans les calculs géométriques. Par la suite, les bornes d'arcade ont réutilisé le hardware des PC et autres consoles de jeux. ==La 3D sur les consoles de quatrième/cinquième génération== Les consoles avant la quatrième génération de console étaient des consoles purement 2D, sans circuits d'accélération 3D. Leur carte graphique était un simple VDC 2D, plus ou moins performant selon la console. Les premières consoles de jeu capables de rendu 3D par elles-mêmes sont les consoles dites de 5ème génération. Il y a diverses manières de classer les consoles en générations, la plus commune place la 3D à la 5ème génération, mais détailler ces controverses quant à ce classement nous amènerait trop loin. Les consoles de génération avaient une architecture assez différente des systèmes antérieurs. Les systèmes SGI et assimilés pouvaient se permettre de couter assez cher, d'utiliser beaucoup de circuits, de prendre beaucoup de place. Les bornes d'arcade sont aussi dans ce cas. Aussi, il n'était pas rare que les cartes 3D de l'époque tiennent sur plusieurs cartes électroniques séparées. Mais une console ne peut pas se permettre ce genre de folies. Aussi, les cartes 3D des consoles de l'époque tenaient dans un seul circuit intégré, comme il est d'usage de nos jours. La conséquence est que certains circuits étaient fortement simplifiés, sur les consoles de cinquième génération. Et cela a impacté l'architecture interne des GPU des consoles. Les systèmes SGI avaient plusieurs processeurs pour calculer la géométrie, couplés à plusieurs unités non-programmables pour les pixels/textures. Les cartes 3D des consoles gardaient cette organisation : processeurs pour la géométrie, circuits fixes pour le reste. Mais elles se débrouillaient souvent avec un seul processeur, voire aucun ! Dans ce dernier cas, la géométrie était calculée sur le processeur principal, le CPU. Les unités pour les pixels étaient aussi moins nombreuses, mais il y en avait plusieurs, pour profiter de l'amplification des pixels. : Les cartes 3D des consoles de jeu utilisaient le placage de texture inverse, avec quelques exceptions qui utilisaient le placage de texture direct. ===Le rendu 3D sur les consoles de quatrième génération : la SNES=== Plus haut, j'ai dit que les consoles de quatrième génération n'avaient pas de carte accélératrice 3D. Pourtant, elles ont connus quelques jeux en vraie 3D. La raison à cela est que la 3D était calculée par un GPU placé dans les cartouches du jeu ! Par exemple, les cartouches de Starfox et de Super Mario 2 contenaient un coprocesseur Super FX, qui gérait des calculs de rendu 2D/3D. En tout, il y a environ 16 coprocesseurs pour la SNES et on en trouve facilement la liste sur le net. La console était conçue pour, des pins sur les ports cartouches étaient prévues pour des fonctionnalités de cartouche annexes, dont ces coprocesseurs. Ces pins connectaient le coprocesseur au bus des entrées-sorties. Les coprocesseurs des cartouches de NES avaient souvent de la mémoire rien que pour eux, qui était intégrée dans la cartouche. Ceci étant dit, passons aux consoles de cinquième génération. ===La Nintendo 64 : un GPU avancé=== La Nintendo 64 avait le GPU le plus complexe comparé aux autres consoles, et dépassait même les cartes graphiques des PC. Il faut dire que son GPU a été conçu avec l'aide de l'entreprise SGI, dont on a vu les systèmes graphiques plus haut. Le GPU de la N64 incorporait une unité pour les calculs géométriques, un circuit de rasterisation, une unité de textures et un ROP final pour les calculs de transparence/brouillard/antialiasing, ainsi qu'un circuit pour gérer la profondeur des pixels. En somme, tout le pipeline graphique était implémenté dans le GPU de la Nintendo 64, chose très en avance sur son temps, comparé au PC ou aux autres consoles ! Le GPU est construit autour d'un processeur dédié aux calculs géométriques, le ''Reality Signal Processor'' (RSP), autour duquel on a ajouté des circuits pour le reste du pipeline graphique. L'unité de calcul géométrique est un processeur MIPS R4000, un processeur assez courant à l'époque, auquel on avait retiré quelques fonctionnalités inutiles pour le rendu 3D. Il était couplé à 4 KB de mémoire vidéo, ainsi qu'à 4 KB de mémoire ROM. Le reste du GPU était réalisé avec des circuits fixes. Un point intéressant est que le programme exécuté par le RSP pouvait être programmé ! Le RSP gérait déjà des espèces de proto-shaders, qui étaient appelés des ''[https://ultra64.ca/files/documentation/online-manuals/functions_reference_manual_2.0i/ucode/microcode.html micro-codes]'' dans la documentation de l'époque. La ROM associée au RSP mémorise cinq à sept programmes différents, aux fonctionnalités différentes. * Les microcodes gspFast3D et gspF3DNoN, implémentent un rendu 3D normal, avec des options de ''clipping'' différentes entre les deux. * Le microcode gspTurbo3D fait la même chose, mais avec moins de fonctionnalités et avec une précision réduite. Il ne gère pas le ''clipping'', l'éclairage par pixel, la correction de perspective, l'antialiasing et quelques autres fonctionnalités. Il gère cependant l'éclairage de Gouraud. Il utilise une ''display list'' simplifiée comparé aux deux microcodes précédents. * Le microcode gspZ-Sort effectue une pré-passe z, à savoir qu'il calcule le tampon de profondeur final de la scène 3D, sans rendre l'image. Cela sert à faire une élimination des pixels cachés parfaite, en logiciel. On calcule le tampon de profondeur pour déterminer quels pixels sont visibles, puis une seconde passe rend l'image en, rejetant les pixels non-visibles. * Le microcode gspSprite2D implémente un rendu 2D émulé : les sprites et arrière-plan sont des rectangles texturés. Le microcode gspS2DEX fait la même chose, mais sert à émuler le rendu de la SNES plus qu'autre chose. * Le microcode gspLine3D ne gére que des lignes, pas de triangles. Il sert pour du rendu en fil de fer. Ils géraient le rendu 3D de manière différente et avec une gestion des ressources différentes. Très peu de studios de jeu vidéo ont développé leur propre microcodes N64, car la documentation était mal faite, que Nintendo ne fournissait pas de support officiel pour cela, que les outils de développement ne permettaient pas de faire cela proprement et efficacement. ===La Playstation 1=== Sur la Playstation 1 le calcul de la géométrie était réalisé par le processeur, la carte graphique gérait tout le reste. Et la carte graphique était un circuit fixe spécialisé dans la rasterisation et le placage de textures. Elle utilisait, comme la Nintendo 64, le placage de texture inverse, qui est apparu ensuite sur les cartes graphiques. ===La 3DO et la Sega Saturn=== La Sega Saturn et la 3DO étaient les deux seules consoles à utiliser le rendu direct. La géométrie était calculée sur le processeur, même si les consoles utilisaient parfois un CPU dédié au calcul de la géométrie. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. La Sega Saturn incorpore trois processeurs et deux GPU. Les deux GPUs sont nommés le VDP1 et le VDP2. Le VDP1 s'occupe des textures et des sprites, le VDP2 s'occupe uniquement de l'arrière-plan et incorpore un VDC tout ce qu'il y a de plus simple. Ils ne gèrent pas du tout la géométrie, qui est calculée par les trois processeurs. Le troisième processeur, la Saturn Control Unit, est un processeur de type DSP, à savoir un processeur spécialisé dans le traitement de signal. Il est utilisé presque exclusivement pour accélérer les calculs géométriques. Il avait sa propre mémoire RAM dédiée, 32 KB de SRAM, soit une mémoire locale très rapide. Les transferts entre cette RAM et le reste de l'ordinateur était géré par un contrôleur DMA intégré dans le DSP. En somme, il s'agit d'une sorte de processeur spécialisé dans la géométrie, une sorte d'unité géométrique programmable. Mais la géométrie n'était pas forcément calculée que sur ce DSP, mais pouvait être prise en charge par les 3 CPU. ==L'historique des cartes graphiques des PC, avant l'arrivée de Direct X et Open Gl== Sur PC, l'évolution des cartes graphiques a eu du retard par rapport aux consoles. Les PC sont en effet des machines multi-usage, pour lesquelles le jeu vidéo était un cas d'utilisation parmi tant d'autres. Et les consoles étaient la plateforme principale pour jouer à des jeux vidéo, le jeu vidéo PC étant plus marginal. Mais cela ne veut pas dire que le jeu PC n'existait pas, loin de là ! Un problème pour les jeux PC était que l'écosystème des PC était aussi fragmenté en plusieurs machines différentes : machines Apple 1 et 2, ordinateurs Commdore et Amiga, IBM PC et dérivés, etc. Aussi, programmer des jeux PC n'était pas mince affaire, car les problèmes de compatibilité étaient légion. C'est seulement quand la plateforme x86 des IBM PC s'est démocratisée que l'informatique grand public s'est standardisée, réduisant fortement les problèmes de compatibilité. Mais cela n'a pas suffit, il a aussi fallu que les API 3D naissent. Les API 3D comme Direct X et Open GL sont absolument cruciales pour garantir la compatibilité entre plusieurs ordinateurs aux cartes graphiques différentes. Aussi, l'évolution des cartes graphiques pour PC s'est faite main dans la main avec l'évolution des API 3D. Les fonctionnalités des cartes graphiques ont évolué dans le temps, en suivant les évolutions des API 3D. Du moins dans les grandes lignes, car il est arrivé plusieurs fois que des fonctionnalités naissent sur les cartes graphiques, pour que les fabricants forcent la main de Microsoft ou d'Open GL pour les intégrer de force dans les API 3D. Passons. ===L'introduction des premiers jeux 3D : Quake et les drivers miniGL=== L'API OpenGL est née de la main de SGI, encore eux ! SGI avait créé l'API Iris GL pour ses stations de travail Iris Graphics. Iris GL a ensuite été libéré et est devenu le standard Open GL. Open GL existait déjà avant l'apparition des cartes accélératrices 3D. Il y a avait donc déjà un terreau que les programmeurs graphiques pouvaient utiliser. Mais Open GL était surtout utilisé pour des applications industrielles, médicales (imagerie), graphiques ou militaires, pas pour le jeu vidéo. Mais cela changea avec la sortie du jeu Quake, d'IdSoftware, en 1996. Quake pouvait fonctionner en rendu logiciel, mais le programmeur responsable du moteur 3D (le célébre John Carmack) ajouta une version OpenGL du jeu. Il faut dire que le jeu était programmé sur une station de travail compatible avec OpenGL, même si aucune carte accélératrice de l'époque ne supportait OpenGL. C'était là un choix qui se révéla visionnaire. En théorie, le rendu par OpenGL aurait dû se faire intégralement en logiciel, sauf sur quelques rares stations de travail adaptées. Mais les premières cartes graphiques étaient déjà dans les starting blocks. La toute première carte 3D pour PC est la '''Rendition Vérité V1000''', sortie en Septembre 1995, soit quelques mois avant l'arrivée de la Nintendo 64. La Rendition Vérité V1000 contenait un processeur MIPS cadencé à 25 MHz, 4 mébioctets de RAM, une ROM pour le BIOS, et un RAMDAC, rien de plus. C'était un vrai ordinateur complètement programmable de bout en bout, sans aucun circuit fixe. Les programmeurs ne pouvaient cependant pas utiliser cette programmabilité avec des ''shaders'', mais elle permettait à Rendition d'implémenter n'importe quelle API 3D, que ce soit OpenGL, DirectX ou même sa son API propriétaire. La Rendition Vérité avait de bonnes performances pour ce qui est de la géométrie, mais pas pour le reste. Réaliser la rastérisation et le placage de texture en logiciel n'est pas efficace, pareil pour les opérations de fin de pipeline comme l'antialiasing. Le manque d'unités fixes très rapides pour la rastérisation, le placage de texture ou les opérations de fin de pipeline était clairement un gros défaut. Mais la Rendition Vérité était un cas à part, une exception dans le paysage des cartes 3D de l'époque, qui ne faisait rien comme les autres. Les autres cartes graphiques, sorties peu après, étaient les Voodoo de 3dfx, les Riva TNT de NVIDIA, les Rage/3D d'ATI, la Virge/3D de S3, et la Matrox Mystique. Elles avaient choisit le compromis inverse de la Rendition Vérité V1000 : de bonnes performances pour le placage de textures et la rastérization, mais pas pour les calculs géométriques. Pour rappel, les systèmes professionnels et les consoles avaient des processeurs pour la géométrie, et des circuits fixes pour le reste. Les cartes graphiques de PC se passaient des processeurs pour la géométrie, les calculs géométriques étaient réalisés par le CPU. Les toutes premières cartes 3D pour PC contenaient seulement des circuits pour gérer les textures et des ROPs. Elle géraient le ''z-buffer'' en mémoire vidéo, ainsi que des effets de brouillard. Il n'y avait même pas de circuit pour la rastérisation, qui était faite en logiciel, avec les calculs géométriques. [[File:Architecture de base d'une carte 3D - 2.png|centre|vignette|upright=1.5|Carte 3D sans rasterization matérielle.]] Les cartes suivantes ajoutèrent une gestion des étapes de ''rasterization'' directement en matériel. Les cartes ATI rage 2, les Invention de chez Rendition, et d'autres cartes graphiques supportaient la rasterisation en hardware. [[File:Architecture de base d'une carte 3D - 3.png|centre|vignette|upright=1.5|Carte 3D avec gestion de la géométrie.]] Pour exploiter les unités de texture et le circuit de rastérisation, OpenGL et Direct 3D étaient partiellement implémentées en logiciel, car les cartes graphiques ne supportaient pas toutes les fonctionnalités de l'API. C'était l'époque du miniGL, des implémentations partielles d'OpenGL, fournies par les fabricants de cartes 3D, implémentées dans les pilotes de périphériques de ces dernières. Les fonctionnalités d'OpenGL implémentées dans ces pilotes étaient presque toutes exécutées en matériel, par la carte graphique. Avec l'évolution du matériel, les pilotes de périphériques devinrent de plus en plus complets, au point de devenir des implémentations totales d'OpenGL. Mais au-delà d'OpenGL, chaque fabricant de carte graphique avait sa propre API propriétaire, qui était gérée par leurs pilotes de périphériques (''drivers''). Par exemple, les premières cartes graphiques de 3dfx interactive, les fameuses voodoo, disposaient de leur propre API graphique, l'API Glide. Elle facilitait la gestion de la géométrie et des textures, ce qui collait bien avec l'architecture de ces cartes 3D. Mais ces API propriétaires tombèrent rapidement en désuétude avec l'évolution de DirectX et d'OpenGL. Direct X était une API dans l'ombre d'Open GL. La première version de Direct X qui supportait la 3D était DirectX 2.0 (juin 2, 1996), suivie rapidement par DirectX 3.0 (septembre 1996). Elles dataient d'avant le jeu Quake, et elles étaient très éloignées du hardware des premières cartes graphiques. Elles utilisaient un système d'''execute buffer'' pour communiquer avec la carte graphique, Microsoft espérait que le matériel 3D implémenterait ce genre de système. Ce qui ne fu pas le cas. Direct X 4.0 a été abandonné en cours de développement pour laisser à une version 5.0 assez semblable à la 2.0/3.0. Le mode de rendu laissait de côté les ''execute buffer'' pour coller un peu plus au hardware de l'époque. Mais rien de vraiment probant comparé à Open GL. Même Windows utilisait Open GL au lieu de Direct X maison... C'est avec Direct X 6.0 que Direct X est entré dans la cours des grands. Il gérait la plupart des technologies supportées par les cartes graphiques de l'époque. ===Le ''multi-texturing'' de l'époque Direct X 6.0 : combiner plusieurs textures=== Une technologie très importante standardisée par Dirext X 6 est la technique du '''''multi-texturing'''''. Avec ce qu'on a dit dans le chapitre précédent, vous pensez sans doute qu'il n'y a qu'une seule texture par objet, qui est plaquée sur sa surface. Mais divers effet graphiques demandent d'ajouter des textures par dessus d'autres textures. En général, elles servent pour ajouter des détails, du relief, sur une surface pré-existante. Un exemple intéressant vient des jeux de tir : ajouter des impacts de balles sur les murs. Pour cela, on plaque une texture d'impact de balle sur le mur, à la position du tir. Il s'agit là d'un exemple de '''''decals''''', des petites textures ajoutées sur les murs ou le sol, afin de simuler de la poussière, des impacts de balle, des craquelures, des fissures, des trous, etc. Les textures en question sont de petite taille et se superposent à une texture existante, plus grande. Rendre des ''decals'' demande de pouvoir superposer deux textures. Direct X 6.0 supportait l'application de plusieurs textures directement dans le matériel. La carte graphique devait être capable d'accéder à deux textures en même temps, ou du moins faire semblant que. Pour cela, elle doublaient les unités de texture et adaptaient les connexions entre unités de texture et mémoire vidéo. La mémoire vidéo devait être capable de gérer plusieurs accès mémoire en même temps et devait alors avoir un débit binaire élevé. [[File:Multitexturing.png|centre|vignette|upright=2|Multitexturing]] La carte graphique devait aussi gérer de quoi combiner deux textures entre elles. Par exemple, pour revenir sur l'exemple d'une texture d'impact de balle, il faut que la texture d'impact recouvre totalement la texture du mur. Dans ce cas, la combinaison est simple : la première texture remplace l'ancienne, là où elle est appliquée. Mais les cartes graphiques ont ajouté d'autres combinaisons possibles, par exemple additionner les deux textures entre elle, faire une moyenne des texels, etc. Les opérations pour combiner les textures était le fait de circuits appelés des '''''combiners'''''. Concrètement, les ''combiners'' sont de simples unités de calcul. Les ''conbiners'' ont beaucoup évolués dans le temps, mais les premières implémentation se limitaient à quelques opérations simples : addition, multiplication, superposition, interpolation. L'opération effectuer était envoyée au ''conbiner'' sur une entrée dédiée. [[File:Multitexturing avec combiners.png|centre|vignette|upright=2|Multitexturing avec combiners]] S'il y avait eu un seul ''conbiner'', le circuit de ''multitexturing'' aurait été simplement configurable. Mais dans la réalité, les premières cartes utilisant du ''multi-texturing'' utilisaient plusieurs ''combiners'' placés les uns à la suite des autres. L'implémentation des ''combiners'' retenue par Open Gl, et par le hardware des cartes graphiques, était la suivante. Les ''combiners'' étaient placés en série, l'un à la suite de l'autre, chacun combinant le résultat de l'étage précédent avec une texture. Le premier ''combiner'' gérait l'éclairage par sommet, afin de conserver un minimum de rétrocompatibilité. [[File:Texture combiners Open GL.png|centre|vignette|upright=2|Texture combiners Open GL]] Voici les opérations supportées par les ''combiners'' d'Open GL. Ils prennent en entrée le résultat de l'étage précédent et le combinent avec une texture lue depuis l'unité de texture. {|class="wikitable" |+ Opérations supportées par les ''combiners'' d'Open GL |- ! Replace | colspan="2" | Pixel provenant de l'unité de texture |- ! Addition | colspan="2" | Additionne l'entrée au texel lu. |- ! Modulate | colspan="2" | Multiplie l'entrée avec le texel lu |- ! Mélange (''blending'') | Moyenne pondérée des deux entrées, pondérée par la composante de transparence || La couleur de transparence du texel lu et de l'entrée sont multipliées. |- ! Decals | Moyenne pondérée des deux entrées, pondérée par la composante de transparence. || La transparence du résultat est celle de l'entrée. |} Il faut noter qu'un dernier étage de ''combiners'' s'occupait d'ajouter la couleur spéculaire et les effets de brouillards. Il était à part des autres et n'était pas configurable, c'était un étage fixe, qui était toujours présent, peu importe le nombre de textures utilisé. Il était parfois appelé le '''''combiner'' final''', terme que nous réutiliserons par la suite. Mine de rien, cela a rendu les cartes graphiques partiellement programmables. Le fait qu'il y ait des opérations enchainées à la suite, opérations qu'on peut choisir librement, suffit à créer une sorte de mini-programme qui décide comment mélanger plusieurs textures. Mais il y avait une limitation de taille : le fait que les données soient transmises d'un étage à l'autre, sans détours possibles. Par exemple, le troisième étage ne pouvait avoir comme seule opérande le résultat du second étage, mais ne pouvait pas utiliser celui du premier étage. Il n'y avait pas de registres pour stocker ce qui sortait de la rastérisation, ni pour mémoriser temporairement les texels lus. ===Le ''Transform & Lighting'' matériel de Direct X 7.0=== [[File:Architecture de base d'une carte 3D - 4.png|vignette|upright=1.5|Carte 3D avec gestion de la géométrie.]] La première carte graphique pour PC capable de gérer la géométrie en hardware fût la Geforce 256, la toute première Geforce. Son unité de gestion de la géométrie n'est autre que la bien connue '''unité T&L''' (''Transform And Lighting''). Elle implémentait des algorithmes d'éclairage de la scène 3D assez simples, comme un éclairage de Gouraud, qui étaient directement câblés dans ses circuits. Mais contrairement à la Nintendo 64 et aux bornes d'arcade, elle implémentait le tout, non pas avec un processeur classique, mais avec des circuits fixes. Avec Direct X 7.0 et Open GL 1.0, l'éclairage était en théorie limité à de l'éclairage par sommet, l'éclairage par pixel n'était pas implémentable en hardware. Les cartes graphiques ont tenté d'implémenter l'éclairage par pixel, mais cela n'est pas allé au-delà du support de quelques techniques de ''bump-mapping'' très limitées. Par exemple, Direct X 6.0 implémentait une forme limitée de ''bump-mapping'', guère plus. Un autre problème est qu'il a beaucoup d'algorithmes d'éclairages différents, aux résultats visuels différents, bien au-delà des algorithmes d'éclairage plat, de Gouraud et de Phong. Et les unités de T&L étaient souvent en retard sur les algorithmes logiciels. Les programmeurs avaient le choix entre programmer les algorithmes d’éclairage qu'ils voulaient et les exécuter en logiciel, ou utiliser ceux de l'unité de T&L. Ils choisissaient souvent la première option. Par exemple, Quake 3 Arena et Unreal Tournament n'utilisaient pas les capacités d'éclairage géométrique et préféraient utiliser leurs calculs d'éclairage logiciel fait maison. Cependant, le hardware dépassait les capacités des API et avait déjà commencé à ajouter des capacités de programmation liées au ''multi-texturing''. Les cartes graphiques de l'époque, surtout chez NVIDIA, implémentaient un système de '''''register combiners''''', une forme améliorée de ''texture combiners'', qui permettait de faire une forme limitée d'éclairage par pixel, notamment du vrai ''bump-mampping'', voire du ''normal-mapping''. Mais ce n'était pas totalement supporté par les API 3D de l'époque. Les ''registers combiners'' sont des ''texture combiners'' mais dans lesquels ont aurait retiré la stricte organisation en série. Il y a toujours plusieurs étages à la suite, qui peuvent exécuter chacun une opération, mais tous les étages ont maintenant accès à toutes les textures lues et à tout ce qui sort de la rastérisation, pas seulement au résultat de l'étape précédente. Pour cela, on ajoute des registres pour mémoriser ce qui sort des unités de texture, et pour ce qui sort de la rastérisation. De plus, on ajoute des registres temporaires pour mémoriser les résultats de chaque ''combiner'', de chaque étage. Il faut cependant signaler qu'il existe un ''combiner'' final, séparé des étages qui effectuent des opérations proprement dits. Il s'agit de l'étage qui applique la couleur spéculaire et les effets de brouillards. Il ne peut être utilisé qu'à la toute fin du traitement, en tant que dernier étage, on ne peut pas mettre d'opérations après lui. Sa sortie est directement connectée aux ROPs, pas à des registres. Il faut donc faire la distinction entre les '''''combiners'' généraux''' qui effectuent une opération et mémorisent le résultat dans des registres, et le ''combiner'' final qui envoie le résultat aux ROPs. L'implémentation des ''register combiners'' utilisait un processeur spécialisés dans les traitements sur des pixels, une sorte de proto-processeur de ''shader''. Le processeur supportait des opérations assez complexes : multiplication, produit scalaire, additions. Il s'agissait d'un processeur de type VLIW, qui sera décrit dans quelques chapitres. Mais ce processeur avait des programmes très courts. Les premières cartes NVIDIA, comme les cartes TNT pouvaient exécuter deux opérations à la suite, suivie par l'application de la couleurs spéculaire et du brouillard. En somme, elles étaient limitées à un ''shader'' à deux/trois opérations, mais c'était un début. Le nombre d'opérations consécutives est rapidement passé à 8 sur la Geforce 3. ===L'arrivée des ''shaders'' avec Direct X 8.0=== [[File:Architecture de la Geforce 3.png|vignette|upright=1.5|Architecture de la Geforce 3]] Les ''register combiners'' était un premier pas vers un éclairage programmable. Paradoxalement, l'évolution suivante s'est faite non pas dans l'unité de rastérisation/texture, mais dans l'unité de traitement de la géométrie. La Geforce 3 a remplacé l'unité de T&L par un processeur capable d'exécuter des programmes. Les programmes en question complétaient l'unité de T&L, afin de pouvoir rajouter des techniques d'éclairage plus complexes. Le tout a permis aussi d'ajouter des animations, des effets de fourrures, des ombres par ''shadow volume'', des systèmes de particule évolués, et bien d'autres. À partir de la Geforce 3 de Nvidia, les cartes graphiques sont devenues capables d'exécuter des programmes appelés '''''shaders'''''. Le terme ''shader'' vient de ''shading'' : ombrage en anglais. Grace aux ''shaders'', l'éclairage est devenu programmable, il n'est plus géré par des unités d'éclairage fixes mais été laissé à la créativité des programmeurs. Les programmeurs ne sont plus vraiment limités par les algorithmes d'éclairage implémentés dans les cartes graphiques, mais peuvent implémenter les algorithmes d'éclairage qu'ils veulent et peuvent le faire exécuter directement sur la carte graphique. Les ''shaders'' sont classifiés suivant les données qu'ils manipulent : '''''pixel shader''''' pour ceux qui manipulent des pixels, '''''vertex shaders''''' pour ceux qui manipulent des sommets. Les premiers sont utilisés pour implémenter l'éclairage par pixel, les autres pour gérer tout ce qui a trait à la géométrie, pas seulement l'éclairage par sommets. Direct X 8.0 avait un standard pour les shaders, appelé ''shaders 1.0'', qui correspondait parfaitement à ce dont était capable la Geforce 3. Il standardisait les ''vertex shaders'' de la Geforce 3, mais il a aussi renommé les ''register combiners'' comme étant des ''pixel shaders'' version 1.0. Les ''register combiners'' n'ont pas évolués depuis la Geforce 256, si ce n'est que les programmes sont passés de deux opérations successives à 8, et qu'il y avait possibilité de lire 4 textures en ''multitexturing''. A l'opposé, le processeur de ''vertex shader'' de la Geforce 3 était capable d'exécuter des programmes de 128 opérations consécutives et avait 258 registres différents ! Des ''pixels shaders'' plus évolués sont arrivés avec l'ATI Radeon 8500 et ses dérivés. Elle incorporait la technologie ''SMARTSHADER'' qui remplacait les ''registers combiners'' par un processeur de ''shader'' un peu limité. Un point est que le processeur acceptait de calculer des adresses de texture dans le ''pixel shader''. Avant, les adresses des texels à lire étaient fournis par l'unité de rastérisation et basta. L'avantage est que certains effets graphiques étaient devenus possibles : du ''bump-mapping'' avancé, des textures procédurales, de l'éclairage par pixel anisotrope, du éclairage de Phong réel, etc. Avec la Radeon 8500, le ''pixel shader'' pouvait calculer des adresses, et lire les texels associés à ces adresses calculées. Les ''pixel shaders'' pouvaient lire 6 textures, faire 8 opérations sur les texels lus, puis lire 6 textures avec les adresses calculées à l'étape précédente, et refaire 8 opérations. Quelque chose de limité, donc, mais déjà plus pratique. Les ''pixel shaders'' de ce type ont été standardisé dans Direct X 8.1, sous le nom de ''pixel shaders 1.4''. Encore une fois, le hardware a forcé l'intégration dans une API 3D. ===Les ''shaders'' de Direct X 9.0 : de vrais ''pixel shaders''=== Avec Direct X 9.0, les ''shaders'' sont devenus de vrais programmes, sans les limitations des ''shaders'' précédents. Les ''pixels shaders'' sont passés à la version 2.0, idem pour les ''vertex shaders''. Concrètement, ils ont des fonctionnalités bien supérieures à celles des ''registers combiners''. Les ''shaders'' pouvaient exécuter une suite d'opérations arbitraire, dans le sens où elle n'était pas structurée avec tel type d'opération au début, suivie par un accès aux textures, etc. On pouvait mettre n'importe quelle opération dans n'importe quel ordre. De plus, les ''shaders'' ne sont plus écrit en assembleur comme c'était le cas avant. Ils sont dorénavant écrits dans un langage de haut-niveau, le HLSL pour les shaders Direct X et le GLSL pour les shaders Open Gl. Les ''shaders'' sont ensuite traduit (compilés) en instructions machines compréhensibles par la carte graphique. Au début, ces langages et la carte graphique supportaient uniquement des opérations simples. Mais au fil du temps, les spécifications de ces langages sont devenues de plus en plus riches à chaque version de Direct X ou d'Open Gl, et le matériel en a fait autant. Le matériel s'est alors adapté, en incorporant un véritable processeur pour les ''pixel shaders''. Les ''pixel shaders'' sont maintenant exécutés par un processeur de ''shader'' dédié, aux fonctionnalités bien supérieures à celles des ''registers combiners''. Le processeur de ''pixel shader'' incorpore l'unité de texture en sont sein, les deux sont fusionnés. La raison à cela sera expliqué dans la suite du chapitre. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=1.5|Carte 3D avec pixels et vertex shaders non-unifiés.]] ===L'après Direct X 9.0=== Avant Direct X 10, les processeurs de ''shaders'' ne géraient pas exactement les mêmes opérations pour les processeurs de ''vertex shader'' et de ''pixel shader''. Les processeurs de ''vertex shader'' et de ''pixel shader''étaient séparés. Depuis DirectX 10, ce n'est plus le cas : le jeu d'instructions a été unifié entre les vertex shaders et les pixels shaders, ce qui fait qu'il n'y a plus de distinction entre processeurs de vertex shaders et de pixels shaders, chaque processeur pouvant traiter indifféremment l'un ou l'autre. [[File:Architecture de base d'une carte 3D - 6.png|centre|vignette|upright=1.5|Architecture de la GeForce 6800.]] Avec Direct X 10, de nombreux autres ''shaders'' sont apparus. Les plus liés au rendu 3D sont les '''''geometry shader''''' pour ceux qui manipulent des triangles, de ''hull shaders'' et de ''domain shaders'' pour la tesselation. De plus, les cartes graphiques modernes sont capables d’exécuter des programmes informatiques qui n'ont aucun lien avec le rendu 3D, mais sont exécutés par la carte graphique comme le ferait un processeur d'ordinateur normal. De tels ''shaders'' sans lien avec le rendu 3D sont appelés des ''compute shader''. ==Les cartes graphiques d'aujourd'hui== Les circuits d'un GPU ont beaucoup évolué depuis l'introduction des ''shaders'', pour devenir de plus en plus programmables. Mais à côté des processeurs de ''shaders'', il reste quelques circuits non-programmables appelés des circuits fixes. La rastérisation, le placage de texture, l'élimination des pixels cachés et le mélange ''alpha'' sont gérés par des circuits fixes. [[File:3D-Pipeline.svg|centre|vignette|upright=3.0|Pipeline 3D : ce qui est programmable et ce qui ne l'est pas dans une carte graphique moderne.]] Mais pourquoi ne pas tout rendre programmable ? Ou au contraire, utiliser seulement des circuits fixes ? La réponse rapide est qu'il s'agit d'un compromis entre flexibilité et performance qui permet d'avoir le meilleur des deux mondes. Mais ce compromis a fortement évolué dans le temps, comme on va le voir plus bas. Rendre l'éclairage programmable permet d'implémenter facilement un grand nombre d'effets graphiques sans avoir à les implémenter en hardware. Avant les ''shaders'', les effets graphiques derniers cri n'étaient disponibles que sur les derniers modèles de carte graphique. Avec des ''vertex/pixel shaders'', ce genre de défaut est passé à la trappe. Si un nouvel algorithme de rendu graphique est inventé, il peut être utilisé dès le lendemain sur toutes les cartes graphiques modernes. De plus, implémenter beaucoup d'algorithmes d'éclairage différents avec des circuits fixes a un cout en termes de transistors, alors qu'utiliser des circuits programmable a un cout en hardware plus limité. Tout cela est à l'exact opposé de ce qu'on a avec les autres circuits, comme les circuits pour la rastérisation ou le placage de texture. Il n'y a pas 36 façons de rastériser une scène 3D et la flexibilité n'est pas un besoin important pour cette opération, alors que les performances sont cruciales. Même chose pour le placage/filtrage de textures. En conséquences, les unités de rastérisation, de texture, et les ROPs sont toutes implémentées en matériel. Faire ainsi permet de gagner en performance sans que cela ait le moindre impact pour le programmeur. Reste à expliquer dans le détail pourquoi. ===Les unités de texture sont intégrées aux processeurs de shaders=== Avec l'arrivée des processeurs de shaders, les unités de texture ont été intégrées dans les processeurs de shaders eux-mêmes. C'est la seule unité fixe qui a subit ce traitement, et il est intéressant de comprendre pourquoi. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=2|Architecture de base d'une carte 3D.]] Pour cela, il faut faire un rappel sur ce qu'il y a dans un processeur. Un processeur contient globalement quatre circuits : * une unité de calcul qui fait des calculs ; * des registres pour stocker les opérandes et résultats des calculs ; * une unité de communication avec la mémoire ; * et un séquenceur, un circuit de contrôle qui commande les autres. L'unité de communication avec la mémoire sert à lire ou écrire des données, à les transférer de la RAM vers les registres, ou l'inverse. Lire une donnée demande d'envoyer son adresse à la RAM, qui répond en envoyant la donnée lue. Elle est donc toute indiquée pour lire une texture : lire une texture n'est qu'un cas particulier de lecture de données. Les texels à lire sont à une adresse précise, la RAM répond à la lecture avec le texel demandé. Il est donc possible d'utiliser l'unité de communication avec la mémoire comme si c'était une unité de texture. Cependant, les textures ne sont pas utilisées comme telles de nos jours. Le rendu 3D moderne utilise des techniques dites de filtrage de texture, qui permettent d'améliorer la qualité du rendu des textures. Sans ce filtrage de texture, les textures appliquées naïvement donnent un résultat assez pixelisé et assez moche, pour des raisons assez techniques. Le filtrage élimine ces artefacts, en utilisant une forme d'''antialiasing'' interne aux textures, le fameux filtrage de texture. Le filtrage de texture peut être réalisé en logiciel ou en matériel. Techniquement, il est possible de le faire dans un ''shader''. Le ''shader'' calcule les adresses des texels à lire, lit les texels, et effectue ensuite le filtrage avec des opérations de calcul. Mais ce n'est pas ce qui est fait, le filtrage de texture est toujours effectué directement en matériel. La raison est que le filtrage de texture est très simple à implémenter en hardware. Le filtrage bilinéaire ou trilinéaire demande juste des circuits d'interpolation et quelques registres, ce qui est trivial. Et la seconde raison est qu'il n'y a pas 36 façons de filtrer des textures : une carte graphique peut implémenter les algorithmes principaux existants en assez peu de circuits. Pour simplifier l'implémentation, les processeurs de ''shader'' modernes disposent d'une unité d'accès mémoire séparée de l'unité de texture. L'unité d'accès mémoire normale s'occupe des accès mémoire hors-textures, alors que l'unité mémoire s'occupe de lire les textures. L'unité de texture contient de quoi faire du filtrage de texture, mais aussi faire des calculs d'adresse spécialisées, intrinsèquement liés au format des textures, qu'on détaillera dans le chapitre sur les textures. En comparaison, les unités d'accès mémoire effectuent des calculs d'adresse plus basiques. Un dernier avantage est que l'unité de texture est reliée au cache de texture, alors que l'unité d'accès mémoire est relié au cache L1/L2. ===Les ROPs peuvent être implémentés dans le ''pixel shader''=== Les ROPs effectuent plusieurs opérations basiques, mais les deux plus importantes sont la gestion du tampon de profondeur et de la transparence. Par transparence, on veut parler du mélange ''alpha''. Pour la gestion du tampon de profondeur, on veut parler du ''z-test'', qui compare la profondeur de deux pixels/fragments. Il s'agit d'opérations simples, qu'un processeur de shader peut faire sans problèmes. Par exemple, le ''z-test'' demande de faire plusieurs étapes : * calculer l'adresse du pixel dans le tampon de profondeur ; * lire le pixel dans le tampon de profondeur ; * Faire la comparaison entre profondeurs ; * Si le résultat de la comparaison est okay : ** écrire la nouvelle valeur z dans le tampon de profondeur, et écrire le nouveau pixel dedans. Le mélange ''alpha'' demande lui de : * calculer l'adresse du pixel dans le ''framebuffer'' ; * lire le pixel dans le ''framebuffer'' ; * faire des additions et multiplications pour le mélange ''alpha'' : * écrire le nouveau pixel dans le ''framebuffer''. Pour résumer il faut pouvoir faire : calcul d'adresse, lecture, écriture, addition, multiplication et comparaisons. Et toutes ces opérations sont supportées nativement par les processeurs de shaders, ce sont des instructions communes. Il est donc possible d'émuler les ROPs dans les pixels shaders. En pratique, c'est assez rare. Les cartes graphiques pour PC ont des ROPs séparés des processeurs de ''shaders''. Par contre, quelques cartes graphiques destinées les smartphones et autres appareils mobiles font exception. Sur celles-ci, les unités de textures et les ROPs sont intégrés dans les processeurs de ''shaders'', aux côtés de l'unité d'accès mémoire LOAD/STORE. Il s'agit de cartes graphiques en rendu à tuiles. {{NavChapitre | book=Les cartes graphiques | prev=Les cartes graphiques : architecture de base | prevText=Les cartes graphiques : architecture de base | next=Les processeurs de shaders | nextText=Les processeurs de shaders }} {{autocat}} 3n3kvenn7tr94m17eabg1o2u3n50iaz 763507 763506 2026-04-11T20:42:20Z Mewtow 31375 /* Les ROPs peuvent être implémentés dans le pixel shader */ 763507 wikitext text/x-wiki Il est intéressant d'étudier le hardware des cartes graphiques en faisant un petit résumé de leur évolution dans le temps. En effet, leur hardware a fortement évolué dans le temps. Et il serait difficile à comprendre le hardware actuel sans parler du hardware d'antan. En effet, une carte graphique moderne est partiellement programmable. Certains circuits sont totalement programmables, d'autres non. Et pour comprendre pourquoi, il faut étudier comment ces circuits ont évolués. Le hardware des cartes graphiques a fortement évolué dans le temps, ce qui n'est pas une surprise. Les évolutions de la technologie, avec la miniaturisation des transistors et l'augmentation de leurs performances a permis aux cartes graphiques d'incorporer de plus en plus de circuits avec les années. Avant l'invention des cartes graphiques, toutes les étapes du pipeline graphique étaient réalisées par le processeur : il calculait l'image à afficher, et l’envoyait à une carte d'affichage 2D. Au fil du temps, de nombreux circuits furent ajoutés, afin de déporter un maximum de calculs vers la carte vidéo. Le rendu 3D moderne est basé sur le placage de texture inverse, avec des coordonnées de texture, une correction de perspective, etc. Mais les anciennes consoles et bornes d'arcade utilisaient le placage de texture direct. Et cela a impacté le hardware des consoles/PCs de l'époque. Avec le placage de texture direct, il était primordial de calculer la géométrie, mais la rasterisation était le fait de VDC améliorés. Aussi, les premières bornes d'arcade 3D et les consoles de 5ème génération disposaient processeurs pour calculer la géométrie et de circuits d'application de textures très particuliers. A l'inverse, les PC utilisaient un rendu inverse, totalement différent. Sur les PC, les premières cartes graphiques avaient un circuit de rastérisation et des unités de textures, mais pas de circuits géométriques. ==Les premières cartes graphiques== Dès les années 70-80, le rendu 3D était utilisé par de nombreuses entreprises industrielles : des applications de visualisation 3D étaient utilisées en architecture, des applications de conception assistée par ordinateur étaient déjà d'utilisation courante, sans compter les simulateurs de vol utilisés par l'armée et les instructeurs qui formaient les pilotes d'avion. Le rendu 3D était aussi étudié au niveau académique, la recherche en 3D était déjà florissante. Il existait même du matériel spécifiquement conçu pour le rendu graphique, mais celui-ci était spécifiquement dédié à des super-calculateurs ou des ''workstations'' (des sortes d'ancêtres des PC, très puissants pour l'époque, mais conçus uniquement pour les entreprises). ===Le début des années 80 : le rendu en fils de fer=== Le tout premier système de ce genre était le '''''Line Drawing System-1''''' de l'entreprise Evans & Sutherland, daté de 1969. Ce n'est ni plus ni moins que le toute premier circuit graphique séparé du processeur ayant existé. C'est en un sens la toute première carte graphique, le tout premier GPU. Il prenait la forme d'un périphérique qui se connectait à l'ordinateur d'un côté et était relié à l'écran de l'autre. Il était compatible avec un grand nombre d'ordinateurs et de processeurs existants. Il a été suivi par plusieurs successeurs, nommés ''Picture System 1, 2'' et le ''PS300 series''. [[File:Evans & Sutherland LDS-1 (1).jpg|vignette|Evans & Sutherland LDS-1 (1)]] Ils permettaient de faire du rendu en fil de fer, sans texture ni même sans polygones colorés. Un tel rendu était utile pour des applications assez limitées : architecture, dessin de molécules pour les entreprises pharmaceutique et certains centres de recherche, l'aérospatiale, etc. Ces cartes graphiques étaient utilisées de concert avec des écrans appelés '''écrans vectoriels''' (''vector display''). Pour simplifier, ils ressemblaient à des écrans CRT, sauf que le faisceau d'électron ne balayait pas l'écran ligne par ligne, mais traçait des lignes arbitraires à l'écran. On lui précisait deux points de coordonnées x1,y1 ; et x2,y2 ; puis l'écran tracait une ligne entre ces deux points. En général, la ligne tracée était maintenue pendant un long moment, entre plusieurs secondes et plusieurs minutes. L'intérieur du circuit était assez simple : un circuit de multiplication de matrice pour les calculs géométriques, un rastériser simplifié (le ''clipping diviser''), un circuit de tracé de lignes, et un processeur de contrôle pour commander les autres circuits. Le fait que ces trois circuits soient séparés permettait une implémentation en pipeline, où plusieurs portions de l'image pouvaient être calculées en même temps : pendant que l'une est dans l'unité géométrique, l'autre est dans le rastériseur et une troisième est en cours de tracé. [[File:Lds1blockdiagram05.svg|centre|vignette|upright=2|Architecture du LDS-1. Le processeur de contrôle n'est pas représenté.]] Le processeur de contrôle exécute un programme qui se charge de commander l'unité géométrique et les autres circuits. Le programme en question est fourni par le programmeur, le LDS-1 est donc totalement programmable. Il lit directement les données nécessaires pour le rendu dans la mémoire de l’ordinateur et le programme exécuté est lui aussi en mémoire principale. Il n'a pas de mémoire vidéo dédiée, il utilise la RAM de l'ordinateur principal. Le multiplieur de matrices est plus complexe qu'on pourrait s'y attendre. Il ne s'agit pas que d'un circuit arithmétique tout simple, mais d'un véritable processeur avec des registres et des instructions machine complexes. Il contient plusieurs registres, l'ensemble mémorisant 4 matrices de 16 nombres chacune (4 lignes de 4 colonnes). Un nombre est codé sur 18 bits. Les registres sont reliés à un ensemble de circuits arithmétiques, des additionneurs et des multiplieurs. Le circuit supporte des instructions de copie entre registres, pour copier une ligne d'une matrice à une autre, des instructions LOAD/STORE pour lire ou écrire dans la mémoire RAM, etc. Il supporte aussi des multiplications en 2D et 3D. Le ''clipping divider'' est un circuit assez complexe, contenant un processeur à accumulateur, une mémoire ROM pour le programme du processeur. Le programme exécuté par le processeur est un petit programme de 62 instructions, stocké dans la ROM. L'algorithme du ''clipping divider'' est décrite dans le papier de recherche "A clipping divider", écrit par Robert Sproull. Un détail assez intéressant est que le résultat en sortie de l'unité géométrique et du rastériseur peuvent être envoyés à l'ordinateur en parallèle du rendu. C'était très utile sur les anciens ordinateurs qui étaient connectés à plusieurs terminaux. Le LDS-1 calculait la géométrie et le rendu, et le tout pouvait petre envoyé à d'autres composants, comme des terminaux, une imprimante, etc. ===Les systèmes ultérieurs : rendu à triangles colorés et texturé=== Les systèmes précédents étaient très limités : ils calculaient la géométrie et n'avaient pas de ''framebuffer'', ni de tampon de profondeur, ni gestion de l'éclairage, ni quoique ce soit. De tels systèmes étaient donc des accélérateurs géométriques que de vrais systèmes graphiques complets, du fait de l'absence de ''framebuffer''. Ils étaient composés de processeurs spécialisés dans les calculs à virgule flottante, faisant des calculs géométriques, et éventuellement d'un processeur pour la rastérisation. La raison est que la RAM était très chère et que créer des circuits fixes étaient très chers et peu disponibles. Par contre, les processeurs à virgule flottante étaient peu chers et facile à trouver. Vers la fin des années 80, grâce à la baisse du prix de la RAM et la démocratisation des ASIC (des circuits fixes fait sur mesure), ajouter un ''framebuffer'' est est devenu possible. C'est alors que sont apparus les '''systèmes de rendu 3D de première génération'''. De tels systèmes ont permis d'implémenter le rendu à primitives colorées qu'on a vu il y a quelques chapitres, à savoir un rendu où les triangles sont coloriés avec une couleur unique. Les systèmes de première génération étaient simples : des processeurs pour le calcul de la géométrie, un circuit de rastérisation, une RAM pour le ''framebuffer'' et des ASIC servant de ROPs très simples. Il n'y avait pas d'élimination des pixels cachés, pas de textures, et encore moins d'éclairage par pixels. Le premier système de ce genre était le ''Shaded Picture System'', toujours par Evans & Sutherland. Il ne gérait pas la couleur et ne pouvait afficher que des images en noir et blanc, mais il gérait l'éclairage par sommet (''vertex lighting''). Il a rapidement été dépassé par les systèmes de l'entreprise ''Silicon Graphics Inc'' (SGI), ainsi que ceux de l'entreprise Apollo avec sa série Apollo DN. Les '''systèmes de seconde génération''' sont apparus vers la fin des années 80, et se distinguent des précédents par l'ajout un tampon de profondeur. Ils intègrent aussi des capacités d'éclairage par pixel, à savoir de l'éclairage plat, de Gouraud, voire de Phong ! Enfin, les '''systèmes de troisième génération''' ont acquis des capacités de placage de texture, que les systèmes précédents n'avaient pas. Ils ont aussi ajouté un support de l'antialiasing. Les systèmes SGI avec placage de texture ont déjà été abordé au chapitre précédent, dans la section sur les GPU en mode immédiat et à ''tile''. Aussi, nous ne reviendrons pas dessus. [[File:Evolution de l'architecture des premières cartes graphiques, dans les années 80-90.png|centre|vignette|upright=2.5|Evolution de l'architecture des premières cartes graphiques, dans les années 80-90]] Les systèmes de première, seconde et troisième génération avaient de nombreux points communs. En premier lieu, ils étaient fabriqués en connectant plusieurs cartes électroniques : une carte pour les calculs géométriques, une ou plusieurs cartes pour le reste du rendu graphique, une carte dédiée au VDC et avec un connecteur écran. Les transistors de l'époque n'étaient pas encore miniaturisés, ce qui fait que le système graphique ne pouvait pas tenir sur une seule carte électronique. Il n'y avait donc pas de carte graphique proprement dit, mais un équivalent éclaté sur plusieurs cartes électroniques. La carte pour la géométrie contenait typiquement une mémoire FIFO pour accumuler les commandes de rendu, un processeur de commande, et plusieurs processeurs géométriques. Les processeurs géométriques étaient parfois conçus sur mesure, comme l'a été le le ''Geometry Engine'' de SGI. Mais il est arrivé qu'ils utilisent des processeurs commerciaux comme le Weitek 3222, l'Intel i860, etc. Les processeurs pouvaient être placés en série ou en parallèle, comme expliqué dans le chapitre précédent. Le circuit de rastérisation était réalisé soit avec un processeur dédié, soit avec un circuit fixe, soit un mélange des deux. La rastérisation est en effet réalisée en plusieurs étapes, certaines peuvent être implémentées avec un processeur et d'autres avec des circuits fixes. Un point important est qu'à l'époque, le rendu n'utilisait pas que des triangles, mais des polygones en général. Ce n'est que par la suite que le rendu s'est focalisé sur les triangles et les ''quads'' (quadrilatères). Il arrivait que le système graphique gérait partiellement des polygones concaves, voire convexes. Sur les systèmes SGI, les calculs géométriques se faisaient avec des polygones, que la rastérisation découpait en triangles, le reste du rendu se faisait avec des triangles. Les stations de travail Apollo DN 10000VS découpaient les polygones en trapézoïdes orientés à l'horizontale, alignés avec des ''scanlines''. D'autres systèmes découpaient tout en triangle lors de l'étape géométrique ==Les précurseurs grand public : les bornes d'arcade== [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] L'accélération du rendu 3D sur les bornes d'arcade était déjà bien avancé dès les années 90. Les bornes d'arcade ont toujours été un segment haut de gamme de l'industrie du jeu vidéo, aussi ce n'est pas étonnant. Le prix d'une borne d'arcade dépassait facilement les 10 000 dollars pour les plus chères et une bonne partie du prix était celui du matériel informatique. Le matériel était donc très puissant et débordait de mémoire RAM comparé aux consoles de jeu et aux PC. La plupart des bornes d'arcade utilisaient du matériel standardisé entre plusieurs bornes. A l'intérieur d'une borne d'arcade se trouve une '''carte de borne d'arcade''' qui est une carte mère avec un ou plusieurs processeurs, de la RAM, une carte graphique, un VDC et pas mal d'autres matériels. La carte est reliée aux périphériques de la borne : joysticks, écran, pédales, le dispositif pour insérer les pièces afin de payer, le système sonore, etc. Le jeu utilisé pour la borne est placé dans une cartouche qui est insérée dans un connecteur spécialisé. Les cartes de bornes d'arcade étaient généralement assez complexes, elles avaient une grande taille et avaient plus de composants que les cartes mères de PC. Chaque carte contenait un grand nombre de chips pour la mémoire RAM et ROM, et il n'était pas rare d'avoir plusieurs processeurs sur une même carte. Et il n'était pas rare d'avoir trois à quatre cartes superposées dans une seule borne. Pour ceux qui veulent en savoir plus, Fabien Sanglard a publié gratuitement un livre sur le fonctionnement des cartes d'arcade CPS System, disponible via ce lien : [https://fabiensanglard.net/b/cpsb.pdf The book of CP System]. Les premières cartes graphiques des bornes d'arcade étaient des cartes graphiques 2D auxquelles on avait ajouté quelques fonctionnalités. Les sprites pouvaient être tournés, agrandit/réduits, ou déformés pour simuler de la perspective et faire de la fausse 3D. Par la suite, le vrai rendu 3D est apparu sur les bornes d'arcade. Dès 1988, la carte d'arcade Namco System 21 et Sega Model 1 géraient les calculs géométriques. Quelques années plus tard, les cartes graphiques se sont mises à supporter un éclairage de Gouraud et du placage de texture. Par exemple, le Namco System 22 et la Sega model 2 supportaient des textures 2D et comme le filtrage de texture (bilinéaire et trilinéaire), le mip-mapping, et quelques autres. Au passage, les cartes graphiques de la Namco System 22 étaient développées en partenariat avec Eans & Sutherland, qui avait commencé à se diversifier dans le marché grand public. Les cartes graphiques de l'époque faisaient les calculs géométriques sur plusieurs processeurs, généralement des processeurs de type DSP (des processeurs spécialisés dans le traitement de signal). Par exemple, la Namco System 2 utilisait 4 DSP de marque Texas Instruments TMS320C25, cadencés à 24,576 MHz. La carte d'arcade Sega Model 1 utilisait quant à elle un DSP spécialisé dans les calculs géométriques. Par la suite, les bornes d'arcade ont réutilisé le hardware des PC et autres consoles de jeux. ==La 3D sur les consoles de quatrième/cinquième génération== Les consoles avant la quatrième génération de console étaient des consoles purement 2D, sans circuits d'accélération 3D. Leur carte graphique était un simple VDC 2D, plus ou moins performant selon la console. Les premières consoles de jeu capables de rendu 3D par elles-mêmes sont les consoles dites de 5ème génération. Il y a diverses manières de classer les consoles en générations, la plus commune place la 3D à la 5ème génération, mais détailler ces controverses quant à ce classement nous amènerait trop loin. Les consoles de génération avaient une architecture assez différente des systèmes antérieurs. Les systèmes SGI et assimilés pouvaient se permettre de couter assez cher, d'utiliser beaucoup de circuits, de prendre beaucoup de place. Les bornes d'arcade sont aussi dans ce cas. Aussi, il n'était pas rare que les cartes 3D de l'époque tiennent sur plusieurs cartes électroniques séparées. Mais une console ne peut pas se permettre ce genre de folies. Aussi, les cartes 3D des consoles de l'époque tenaient dans un seul circuit intégré, comme il est d'usage de nos jours. La conséquence est que certains circuits étaient fortement simplifiés, sur les consoles de cinquième génération. Et cela a impacté l'architecture interne des GPU des consoles. Les systèmes SGI avaient plusieurs processeurs pour calculer la géométrie, couplés à plusieurs unités non-programmables pour les pixels/textures. Les cartes 3D des consoles gardaient cette organisation : processeurs pour la géométrie, circuits fixes pour le reste. Mais elles se débrouillaient souvent avec un seul processeur, voire aucun ! Dans ce dernier cas, la géométrie était calculée sur le processeur principal, le CPU. Les unités pour les pixels étaient aussi moins nombreuses, mais il y en avait plusieurs, pour profiter de l'amplification des pixels. : Les cartes 3D des consoles de jeu utilisaient le placage de texture inverse, avec quelques exceptions qui utilisaient le placage de texture direct. ===Le rendu 3D sur les consoles de quatrième génération : la SNES=== Plus haut, j'ai dit que les consoles de quatrième génération n'avaient pas de carte accélératrice 3D. Pourtant, elles ont connus quelques jeux en vraie 3D. La raison à cela est que la 3D était calculée par un GPU placé dans les cartouches du jeu ! Par exemple, les cartouches de Starfox et de Super Mario 2 contenaient un coprocesseur Super FX, qui gérait des calculs de rendu 2D/3D. En tout, il y a environ 16 coprocesseurs pour la SNES et on en trouve facilement la liste sur le net. La console était conçue pour, des pins sur les ports cartouches étaient prévues pour des fonctionnalités de cartouche annexes, dont ces coprocesseurs. Ces pins connectaient le coprocesseur au bus des entrées-sorties. Les coprocesseurs des cartouches de NES avaient souvent de la mémoire rien que pour eux, qui était intégrée dans la cartouche. Ceci étant dit, passons aux consoles de cinquième génération. ===La Nintendo 64 : un GPU avancé=== La Nintendo 64 avait le GPU le plus complexe comparé aux autres consoles, et dépassait même les cartes graphiques des PC. Il faut dire que son GPU a été conçu avec l'aide de l'entreprise SGI, dont on a vu les systèmes graphiques plus haut. Le GPU de la N64 incorporait une unité pour les calculs géométriques, un circuit de rasterisation, une unité de textures et un ROP final pour les calculs de transparence/brouillard/antialiasing, ainsi qu'un circuit pour gérer la profondeur des pixels. En somme, tout le pipeline graphique était implémenté dans le GPU de la Nintendo 64, chose très en avance sur son temps, comparé au PC ou aux autres consoles ! Le GPU est construit autour d'un processeur dédié aux calculs géométriques, le ''Reality Signal Processor'' (RSP), autour duquel on a ajouté des circuits pour le reste du pipeline graphique. L'unité de calcul géométrique est un processeur MIPS R4000, un processeur assez courant à l'époque, auquel on avait retiré quelques fonctionnalités inutiles pour le rendu 3D. Il était couplé à 4 KB de mémoire vidéo, ainsi qu'à 4 KB de mémoire ROM. Le reste du GPU était réalisé avec des circuits fixes. Un point intéressant est que le programme exécuté par le RSP pouvait être programmé ! Le RSP gérait déjà des espèces de proto-shaders, qui étaient appelés des ''[https://ultra64.ca/files/documentation/online-manuals/functions_reference_manual_2.0i/ucode/microcode.html micro-codes]'' dans la documentation de l'époque. La ROM associée au RSP mémorise cinq à sept programmes différents, aux fonctionnalités différentes. * Les microcodes gspFast3D et gspF3DNoN, implémentent un rendu 3D normal, avec des options de ''clipping'' différentes entre les deux. * Le microcode gspTurbo3D fait la même chose, mais avec moins de fonctionnalités et avec une précision réduite. Il ne gère pas le ''clipping'', l'éclairage par pixel, la correction de perspective, l'antialiasing et quelques autres fonctionnalités. Il gère cependant l'éclairage de Gouraud. Il utilise une ''display list'' simplifiée comparé aux deux microcodes précédents. * Le microcode gspZ-Sort effectue une pré-passe z, à savoir qu'il calcule le tampon de profondeur final de la scène 3D, sans rendre l'image. Cela sert à faire une élimination des pixels cachés parfaite, en logiciel. On calcule le tampon de profondeur pour déterminer quels pixels sont visibles, puis une seconde passe rend l'image en, rejetant les pixels non-visibles. * Le microcode gspSprite2D implémente un rendu 2D émulé : les sprites et arrière-plan sont des rectangles texturés. Le microcode gspS2DEX fait la même chose, mais sert à émuler le rendu de la SNES plus qu'autre chose. * Le microcode gspLine3D ne gére que des lignes, pas de triangles. Il sert pour du rendu en fil de fer. Ils géraient le rendu 3D de manière différente et avec une gestion des ressources différentes. Très peu de studios de jeu vidéo ont développé leur propre microcodes N64, car la documentation était mal faite, que Nintendo ne fournissait pas de support officiel pour cela, que les outils de développement ne permettaient pas de faire cela proprement et efficacement. ===La Playstation 1=== Sur la Playstation 1 le calcul de la géométrie était réalisé par le processeur, la carte graphique gérait tout le reste. Et la carte graphique était un circuit fixe spécialisé dans la rasterisation et le placage de textures. Elle utilisait, comme la Nintendo 64, le placage de texture inverse, qui est apparu ensuite sur les cartes graphiques. ===La 3DO et la Sega Saturn=== La Sega Saturn et la 3DO étaient les deux seules consoles à utiliser le rendu direct. La géométrie était calculée sur le processeur, même si les consoles utilisaient parfois un CPU dédié au calcul de la géométrie. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. La Sega Saturn incorpore trois processeurs et deux GPU. Les deux GPUs sont nommés le VDP1 et le VDP2. Le VDP1 s'occupe des textures et des sprites, le VDP2 s'occupe uniquement de l'arrière-plan et incorpore un VDC tout ce qu'il y a de plus simple. Ils ne gèrent pas du tout la géométrie, qui est calculée par les trois processeurs. Le troisième processeur, la Saturn Control Unit, est un processeur de type DSP, à savoir un processeur spécialisé dans le traitement de signal. Il est utilisé presque exclusivement pour accélérer les calculs géométriques. Il avait sa propre mémoire RAM dédiée, 32 KB de SRAM, soit une mémoire locale très rapide. Les transferts entre cette RAM et le reste de l'ordinateur était géré par un contrôleur DMA intégré dans le DSP. En somme, il s'agit d'une sorte de processeur spécialisé dans la géométrie, une sorte d'unité géométrique programmable. Mais la géométrie n'était pas forcément calculée que sur ce DSP, mais pouvait être prise en charge par les 3 CPU. ==L'historique des cartes graphiques des PC, avant l'arrivée de Direct X et Open Gl== Sur PC, l'évolution des cartes graphiques a eu du retard par rapport aux consoles. Les PC sont en effet des machines multi-usage, pour lesquelles le jeu vidéo était un cas d'utilisation parmi tant d'autres. Et les consoles étaient la plateforme principale pour jouer à des jeux vidéo, le jeu vidéo PC étant plus marginal. Mais cela ne veut pas dire que le jeu PC n'existait pas, loin de là ! Un problème pour les jeux PC était que l'écosystème des PC était aussi fragmenté en plusieurs machines différentes : machines Apple 1 et 2, ordinateurs Commdore et Amiga, IBM PC et dérivés, etc. Aussi, programmer des jeux PC n'était pas mince affaire, car les problèmes de compatibilité étaient légion. C'est seulement quand la plateforme x86 des IBM PC s'est démocratisée que l'informatique grand public s'est standardisée, réduisant fortement les problèmes de compatibilité. Mais cela n'a pas suffit, il a aussi fallu que les API 3D naissent. Les API 3D comme Direct X et Open GL sont absolument cruciales pour garantir la compatibilité entre plusieurs ordinateurs aux cartes graphiques différentes. Aussi, l'évolution des cartes graphiques pour PC s'est faite main dans la main avec l'évolution des API 3D. Les fonctionnalités des cartes graphiques ont évolué dans le temps, en suivant les évolutions des API 3D. Du moins dans les grandes lignes, car il est arrivé plusieurs fois que des fonctionnalités naissent sur les cartes graphiques, pour que les fabricants forcent la main de Microsoft ou d'Open GL pour les intégrer de force dans les API 3D. Passons. ===L'introduction des premiers jeux 3D : Quake et les drivers miniGL=== L'API OpenGL est née de la main de SGI, encore eux ! SGI avait créé l'API Iris GL pour ses stations de travail Iris Graphics. Iris GL a ensuite été libéré et est devenu le standard Open GL. Open GL existait déjà avant l'apparition des cartes accélératrices 3D. Il y a avait donc déjà un terreau que les programmeurs graphiques pouvaient utiliser. Mais Open GL était surtout utilisé pour des applications industrielles, médicales (imagerie), graphiques ou militaires, pas pour le jeu vidéo. Mais cela changea avec la sortie du jeu Quake, d'IdSoftware, en 1996. Quake pouvait fonctionner en rendu logiciel, mais le programmeur responsable du moteur 3D (le célébre John Carmack) ajouta une version OpenGL du jeu. Il faut dire que le jeu était programmé sur une station de travail compatible avec OpenGL, même si aucune carte accélératrice de l'époque ne supportait OpenGL. C'était là un choix qui se révéla visionnaire. En théorie, le rendu par OpenGL aurait dû se faire intégralement en logiciel, sauf sur quelques rares stations de travail adaptées. Mais les premières cartes graphiques étaient déjà dans les starting blocks. La toute première carte 3D pour PC est la '''Rendition Vérité V1000''', sortie en Septembre 1995, soit quelques mois avant l'arrivée de la Nintendo 64. La Rendition Vérité V1000 contenait un processeur MIPS cadencé à 25 MHz, 4 mébioctets de RAM, une ROM pour le BIOS, et un RAMDAC, rien de plus. C'était un vrai ordinateur complètement programmable de bout en bout, sans aucun circuit fixe. Les programmeurs ne pouvaient cependant pas utiliser cette programmabilité avec des ''shaders'', mais elle permettait à Rendition d'implémenter n'importe quelle API 3D, que ce soit OpenGL, DirectX ou même sa son API propriétaire. La Rendition Vérité avait de bonnes performances pour ce qui est de la géométrie, mais pas pour le reste. Réaliser la rastérisation et le placage de texture en logiciel n'est pas efficace, pareil pour les opérations de fin de pipeline comme l'antialiasing. Le manque d'unités fixes très rapides pour la rastérisation, le placage de texture ou les opérations de fin de pipeline était clairement un gros défaut. Mais la Rendition Vérité était un cas à part, une exception dans le paysage des cartes 3D de l'époque, qui ne faisait rien comme les autres. Les autres cartes graphiques, sorties peu après, étaient les Voodoo de 3dfx, les Riva TNT de NVIDIA, les Rage/3D d'ATI, la Virge/3D de S3, et la Matrox Mystique. Elles avaient choisit le compromis inverse de la Rendition Vérité V1000 : de bonnes performances pour le placage de textures et la rastérization, mais pas pour les calculs géométriques. Pour rappel, les systèmes professionnels et les consoles avaient des processeurs pour la géométrie, et des circuits fixes pour le reste. Les cartes graphiques de PC se passaient des processeurs pour la géométrie, les calculs géométriques étaient réalisés par le CPU. Les toutes premières cartes 3D pour PC contenaient seulement des circuits pour gérer les textures et des ROPs. Elle géraient le ''z-buffer'' en mémoire vidéo, ainsi que des effets de brouillard. Il n'y avait même pas de circuit pour la rastérisation, qui était faite en logiciel, avec les calculs géométriques. [[File:Architecture de base d'une carte 3D - 2.png|centre|vignette|upright=1.5|Carte 3D sans rasterization matérielle.]] Les cartes suivantes ajoutèrent une gestion des étapes de ''rasterization'' directement en matériel. Les cartes ATI rage 2, les Invention de chez Rendition, et d'autres cartes graphiques supportaient la rasterisation en hardware. [[File:Architecture de base d'une carte 3D - 3.png|centre|vignette|upright=1.5|Carte 3D avec gestion de la géométrie.]] Pour exploiter les unités de texture et le circuit de rastérisation, OpenGL et Direct 3D étaient partiellement implémentées en logiciel, car les cartes graphiques ne supportaient pas toutes les fonctionnalités de l'API. C'était l'époque du miniGL, des implémentations partielles d'OpenGL, fournies par les fabricants de cartes 3D, implémentées dans les pilotes de périphériques de ces dernières. Les fonctionnalités d'OpenGL implémentées dans ces pilotes étaient presque toutes exécutées en matériel, par la carte graphique. Avec l'évolution du matériel, les pilotes de périphériques devinrent de plus en plus complets, au point de devenir des implémentations totales d'OpenGL. Mais au-delà d'OpenGL, chaque fabricant de carte graphique avait sa propre API propriétaire, qui était gérée par leurs pilotes de périphériques (''drivers''). Par exemple, les premières cartes graphiques de 3dfx interactive, les fameuses voodoo, disposaient de leur propre API graphique, l'API Glide. Elle facilitait la gestion de la géométrie et des textures, ce qui collait bien avec l'architecture de ces cartes 3D. Mais ces API propriétaires tombèrent rapidement en désuétude avec l'évolution de DirectX et d'OpenGL. Direct X était une API dans l'ombre d'Open GL. La première version de Direct X qui supportait la 3D était DirectX 2.0 (juin 2, 1996), suivie rapidement par DirectX 3.0 (septembre 1996). Elles dataient d'avant le jeu Quake, et elles étaient très éloignées du hardware des premières cartes graphiques. Elles utilisaient un système d'''execute buffer'' pour communiquer avec la carte graphique, Microsoft espérait que le matériel 3D implémenterait ce genre de système. Ce qui ne fu pas le cas. Direct X 4.0 a été abandonné en cours de développement pour laisser à une version 5.0 assez semblable à la 2.0/3.0. Le mode de rendu laissait de côté les ''execute buffer'' pour coller un peu plus au hardware de l'époque. Mais rien de vraiment probant comparé à Open GL. Même Windows utilisait Open GL au lieu de Direct X maison... C'est avec Direct X 6.0 que Direct X est entré dans la cours des grands. Il gérait la plupart des technologies supportées par les cartes graphiques de l'époque. ===Le ''multi-texturing'' de l'époque Direct X 6.0 : combiner plusieurs textures=== Une technologie très importante standardisée par Dirext X 6 est la technique du '''''multi-texturing'''''. Avec ce qu'on a dit dans le chapitre précédent, vous pensez sans doute qu'il n'y a qu'une seule texture par objet, qui est plaquée sur sa surface. Mais divers effet graphiques demandent d'ajouter des textures par dessus d'autres textures. En général, elles servent pour ajouter des détails, du relief, sur une surface pré-existante. Un exemple intéressant vient des jeux de tir : ajouter des impacts de balles sur les murs. Pour cela, on plaque une texture d'impact de balle sur le mur, à la position du tir. Il s'agit là d'un exemple de '''''decals''''', des petites textures ajoutées sur les murs ou le sol, afin de simuler de la poussière, des impacts de balle, des craquelures, des fissures, des trous, etc. Les textures en question sont de petite taille et se superposent à une texture existante, plus grande. Rendre des ''decals'' demande de pouvoir superposer deux textures. Direct X 6.0 supportait l'application de plusieurs textures directement dans le matériel. La carte graphique devait être capable d'accéder à deux textures en même temps, ou du moins faire semblant que. Pour cela, elle doublaient les unités de texture et adaptaient les connexions entre unités de texture et mémoire vidéo. La mémoire vidéo devait être capable de gérer plusieurs accès mémoire en même temps et devait alors avoir un débit binaire élevé. [[File:Multitexturing.png|centre|vignette|upright=2|Multitexturing]] La carte graphique devait aussi gérer de quoi combiner deux textures entre elles. Par exemple, pour revenir sur l'exemple d'une texture d'impact de balle, il faut que la texture d'impact recouvre totalement la texture du mur. Dans ce cas, la combinaison est simple : la première texture remplace l'ancienne, là où elle est appliquée. Mais les cartes graphiques ont ajouté d'autres combinaisons possibles, par exemple additionner les deux textures entre elle, faire une moyenne des texels, etc. Les opérations pour combiner les textures était le fait de circuits appelés des '''''combiners'''''. Concrètement, les ''combiners'' sont de simples unités de calcul. Les ''conbiners'' ont beaucoup évolués dans le temps, mais les premières implémentation se limitaient à quelques opérations simples : addition, multiplication, superposition, interpolation. L'opération effectuer était envoyée au ''conbiner'' sur une entrée dédiée. [[File:Multitexturing avec combiners.png|centre|vignette|upright=2|Multitexturing avec combiners]] S'il y avait eu un seul ''conbiner'', le circuit de ''multitexturing'' aurait été simplement configurable. Mais dans la réalité, les premières cartes utilisant du ''multi-texturing'' utilisaient plusieurs ''combiners'' placés les uns à la suite des autres. L'implémentation des ''combiners'' retenue par Open Gl, et par le hardware des cartes graphiques, était la suivante. Les ''combiners'' étaient placés en série, l'un à la suite de l'autre, chacun combinant le résultat de l'étage précédent avec une texture. Le premier ''combiner'' gérait l'éclairage par sommet, afin de conserver un minimum de rétrocompatibilité. [[File:Texture combiners Open GL.png|centre|vignette|upright=2|Texture combiners Open GL]] Voici les opérations supportées par les ''combiners'' d'Open GL. Ils prennent en entrée le résultat de l'étage précédent et le combinent avec une texture lue depuis l'unité de texture. {|class="wikitable" |+ Opérations supportées par les ''combiners'' d'Open GL |- ! Replace | colspan="2" | Pixel provenant de l'unité de texture |- ! Addition | colspan="2" | Additionne l'entrée au texel lu. |- ! Modulate | colspan="2" | Multiplie l'entrée avec le texel lu |- ! Mélange (''blending'') | Moyenne pondérée des deux entrées, pondérée par la composante de transparence || La couleur de transparence du texel lu et de l'entrée sont multipliées. |- ! Decals | Moyenne pondérée des deux entrées, pondérée par la composante de transparence. || La transparence du résultat est celle de l'entrée. |} Il faut noter qu'un dernier étage de ''combiners'' s'occupait d'ajouter la couleur spéculaire et les effets de brouillards. Il était à part des autres et n'était pas configurable, c'était un étage fixe, qui était toujours présent, peu importe le nombre de textures utilisé. Il était parfois appelé le '''''combiner'' final''', terme que nous réutiliserons par la suite. Mine de rien, cela a rendu les cartes graphiques partiellement programmables. Le fait qu'il y ait des opérations enchainées à la suite, opérations qu'on peut choisir librement, suffit à créer une sorte de mini-programme qui décide comment mélanger plusieurs textures. Mais il y avait une limitation de taille : le fait que les données soient transmises d'un étage à l'autre, sans détours possibles. Par exemple, le troisième étage ne pouvait avoir comme seule opérande le résultat du second étage, mais ne pouvait pas utiliser celui du premier étage. Il n'y avait pas de registres pour stocker ce qui sortait de la rastérisation, ni pour mémoriser temporairement les texels lus. ===Le ''Transform & Lighting'' matériel de Direct X 7.0=== [[File:Architecture de base d'une carte 3D - 4.png|vignette|upright=1.5|Carte 3D avec gestion de la géométrie.]] La première carte graphique pour PC capable de gérer la géométrie en hardware fût la Geforce 256, la toute première Geforce. Son unité de gestion de la géométrie n'est autre que la bien connue '''unité T&L''' (''Transform And Lighting''). Elle implémentait des algorithmes d'éclairage de la scène 3D assez simples, comme un éclairage de Gouraud, qui étaient directement câblés dans ses circuits. Mais contrairement à la Nintendo 64 et aux bornes d'arcade, elle implémentait le tout, non pas avec un processeur classique, mais avec des circuits fixes. Avec Direct X 7.0 et Open GL 1.0, l'éclairage était en théorie limité à de l'éclairage par sommet, l'éclairage par pixel n'était pas implémentable en hardware. Les cartes graphiques ont tenté d'implémenter l'éclairage par pixel, mais cela n'est pas allé au-delà du support de quelques techniques de ''bump-mapping'' très limitées. Par exemple, Direct X 6.0 implémentait une forme limitée de ''bump-mapping'', guère plus. Un autre problème est qu'il a beaucoup d'algorithmes d'éclairages différents, aux résultats visuels différents, bien au-delà des algorithmes d'éclairage plat, de Gouraud et de Phong. Et les unités de T&L étaient souvent en retard sur les algorithmes logiciels. Les programmeurs avaient le choix entre programmer les algorithmes d’éclairage qu'ils voulaient et les exécuter en logiciel, ou utiliser ceux de l'unité de T&L. Ils choisissaient souvent la première option. Par exemple, Quake 3 Arena et Unreal Tournament n'utilisaient pas les capacités d'éclairage géométrique et préféraient utiliser leurs calculs d'éclairage logiciel fait maison. Cependant, le hardware dépassait les capacités des API et avait déjà commencé à ajouter des capacités de programmation liées au ''multi-texturing''. Les cartes graphiques de l'époque, surtout chez NVIDIA, implémentaient un système de '''''register combiners''''', une forme améliorée de ''texture combiners'', qui permettait de faire une forme limitée d'éclairage par pixel, notamment du vrai ''bump-mampping'', voire du ''normal-mapping''. Mais ce n'était pas totalement supporté par les API 3D de l'époque. Les ''registers combiners'' sont des ''texture combiners'' mais dans lesquels ont aurait retiré la stricte organisation en série. Il y a toujours plusieurs étages à la suite, qui peuvent exécuter chacun une opération, mais tous les étages ont maintenant accès à toutes les textures lues et à tout ce qui sort de la rastérisation, pas seulement au résultat de l'étape précédente. Pour cela, on ajoute des registres pour mémoriser ce qui sort des unités de texture, et pour ce qui sort de la rastérisation. De plus, on ajoute des registres temporaires pour mémoriser les résultats de chaque ''combiner'', de chaque étage. Il faut cependant signaler qu'il existe un ''combiner'' final, séparé des étages qui effectuent des opérations proprement dits. Il s'agit de l'étage qui applique la couleur spéculaire et les effets de brouillards. Il ne peut être utilisé qu'à la toute fin du traitement, en tant que dernier étage, on ne peut pas mettre d'opérations après lui. Sa sortie est directement connectée aux ROPs, pas à des registres. Il faut donc faire la distinction entre les '''''combiners'' généraux''' qui effectuent une opération et mémorisent le résultat dans des registres, et le ''combiner'' final qui envoie le résultat aux ROPs. L'implémentation des ''register combiners'' utilisait un processeur spécialisés dans les traitements sur des pixels, une sorte de proto-processeur de ''shader''. Le processeur supportait des opérations assez complexes : multiplication, produit scalaire, additions. Il s'agissait d'un processeur de type VLIW, qui sera décrit dans quelques chapitres. Mais ce processeur avait des programmes très courts. Les premières cartes NVIDIA, comme les cartes TNT pouvaient exécuter deux opérations à la suite, suivie par l'application de la couleurs spéculaire et du brouillard. En somme, elles étaient limitées à un ''shader'' à deux/trois opérations, mais c'était un début. Le nombre d'opérations consécutives est rapidement passé à 8 sur la Geforce 3. ===L'arrivée des ''shaders'' avec Direct X 8.0=== [[File:Architecture de la Geforce 3.png|vignette|upright=1.5|Architecture de la Geforce 3]] Les ''register combiners'' était un premier pas vers un éclairage programmable. Paradoxalement, l'évolution suivante s'est faite non pas dans l'unité de rastérisation/texture, mais dans l'unité de traitement de la géométrie. La Geforce 3 a remplacé l'unité de T&L par un processeur capable d'exécuter des programmes. Les programmes en question complétaient l'unité de T&L, afin de pouvoir rajouter des techniques d'éclairage plus complexes. Le tout a permis aussi d'ajouter des animations, des effets de fourrures, des ombres par ''shadow volume'', des systèmes de particule évolués, et bien d'autres. À partir de la Geforce 3 de Nvidia, les cartes graphiques sont devenues capables d'exécuter des programmes appelés '''''shaders'''''. Le terme ''shader'' vient de ''shading'' : ombrage en anglais. Grace aux ''shaders'', l'éclairage est devenu programmable, il n'est plus géré par des unités d'éclairage fixes mais été laissé à la créativité des programmeurs. Les programmeurs ne sont plus vraiment limités par les algorithmes d'éclairage implémentés dans les cartes graphiques, mais peuvent implémenter les algorithmes d'éclairage qu'ils veulent et peuvent le faire exécuter directement sur la carte graphique. Les ''shaders'' sont classifiés suivant les données qu'ils manipulent : '''''pixel shader''''' pour ceux qui manipulent des pixels, '''''vertex shaders''''' pour ceux qui manipulent des sommets. Les premiers sont utilisés pour implémenter l'éclairage par pixel, les autres pour gérer tout ce qui a trait à la géométrie, pas seulement l'éclairage par sommets. Direct X 8.0 avait un standard pour les shaders, appelé ''shaders 1.0'', qui correspondait parfaitement à ce dont était capable la Geforce 3. Il standardisait les ''vertex shaders'' de la Geforce 3, mais il a aussi renommé les ''register combiners'' comme étant des ''pixel shaders'' version 1.0. Les ''register combiners'' n'ont pas évolués depuis la Geforce 256, si ce n'est que les programmes sont passés de deux opérations successives à 8, et qu'il y avait possibilité de lire 4 textures en ''multitexturing''. A l'opposé, le processeur de ''vertex shader'' de la Geforce 3 était capable d'exécuter des programmes de 128 opérations consécutives et avait 258 registres différents ! Des ''pixels shaders'' plus évolués sont arrivés avec l'ATI Radeon 8500 et ses dérivés. Elle incorporait la technologie ''SMARTSHADER'' qui remplacait les ''registers combiners'' par un processeur de ''shader'' un peu limité. Un point est que le processeur acceptait de calculer des adresses de texture dans le ''pixel shader''. Avant, les adresses des texels à lire étaient fournis par l'unité de rastérisation et basta. L'avantage est que certains effets graphiques étaient devenus possibles : du ''bump-mapping'' avancé, des textures procédurales, de l'éclairage par pixel anisotrope, du éclairage de Phong réel, etc. Avec la Radeon 8500, le ''pixel shader'' pouvait calculer des adresses, et lire les texels associés à ces adresses calculées. Les ''pixel shaders'' pouvaient lire 6 textures, faire 8 opérations sur les texels lus, puis lire 6 textures avec les adresses calculées à l'étape précédente, et refaire 8 opérations. Quelque chose de limité, donc, mais déjà plus pratique. Les ''pixel shaders'' de ce type ont été standardisé dans Direct X 8.1, sous le nom de ''pixel shaders 1.4''. Encore une fois, le hardware a forcé l'intégration dans une API 3D. ===Les ''shaders'' de Direct X 9.0 : de vrais ''pixel shaders''=== Avec Direct X 9.0, les ''shaders'' sont devenus de vrais programmes, sans les limitations des ''shaders'' précédents. Les ''pixels shaders'' sont passés à la version 2.0, idem pour les ''vertex shaders''. Concrètement, ils ont des fonctionnalités bien supérieures à celles des ''registers combiners''. Les ''shaders'' pouvaient exécuter une suite d'opérations arbitraire, dans le sens où elle n'était pas structurée avec tel type d'opération au début, suivie par un accès aux textures, etc. On pouvait mettre n'importe quelle opération dans n'importe quel ordre. De plus, les ''shaders'' ne sont plus écrit en assembleur comme c'était le cas avant. Ils sont dorénavant écrits dans un langage de haut-niveau, le HLSL pour les shaders Direct X et le GLSL pour les shaders Open Gl. Les ''shaders'' sont ensuite traduit (compilés) en instructions machines compréhensibles par la carte graphique. Au début, ces langages et la carte graphique supportaient uniquement des opérations simples. Mais au fil du temps, les spécifications de ces langages sont devenues de plus en plus riches à chaque version de Direct X ou d'Open Gl, et le matériel en a fait autant. Le matériel s'est alors adapté, en incorporant un véritable processeur pour les ''pixel shaders''. Les ''pixel shaders'' sont maintenant exécutés par un processeur de ''shader'' dédié, aux fonctionnalités bien supérieures à celles des ''registers combiners''. Le processeur de ''pixel shader'' incorpore l'unité de texture en sont sein, les deux sont fusionnés. La raison à cela sera expliqué dans la suite du chapitre. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=1.5|Carte 3D avec pixels et vertex shaders non-unifiés.]] ===L'après Direct X 9.0=== Avant Direct X 10, les processeurs de ''shaders'' ne géraient pas exactement les mêmes opérations pour les processeurs de ''vertex shader'' et de ''pixel shader''. Les processeurs de ''vertex shader'' et de ''pixel shader''étaient séparés. Depuis DirectX 10, ce n'est plus le cas : le jeu d'instructions a été unifié entre les vertex shaders et les pixels shaders, ce qui fait qu'il n'y a plus de distinction entre processeurs de vertex shaders et de pixels shaders, chaque processeur pouvant traiter indifféremment l'un ou l'autre. [[File:Architecture de base d'une carte 3D - 6.png|centre|vignette|upright=1.5|Architecture de la GeForce 6800.]] Avec Direct X 10, de nombreux autres ''shaders'' sont apparus. Les plus liés au rendu 3D sont les '''''geometry shader''''' pour ceux qui manipulent des triangles, de ''hull shaders'' et de ''domain shaders'' pour la tesselation. De plus, les cartes graphiques modernes sont capables d’exécuter des programmes informatiques qui n'ont aucun lien avec le rendu 3D, mais sont exécutés par la carte graphique comme le ferait un processeur d'ordinateur normal. De tels ''shaders'' sans lien avec le rendu 3D sont appelés des ''compute shader''. ==Les cartes graphiques d'aujourd'hui== Les circuits d'un GPU ont beaucoup évolué depuis l'introduction des ''shaders'', pour devenir de plus en plus programmables. Mais à côté des processeurs de ''shaders'', il reste quelques circuits non-programmables appelés des circuits fixes. La rastérisation, le placage de texture, l'élimination des pixels cachés et le mélange ''alpha'' sont gérés par des circuits fixes. [[File:3D-Pipeline.svg|centre|vignette|upright=3.0|Pipeline 3D : ce qui est programmable et ce qui ne l'est pas dans une carte graphique moderne.]] Mais pourquoi ne pas tout rendre programmable ? Ou au contraire, utiliser seulement des circuits fixes ? La réponse rapide est qu'il s'agit d'un compromis entre flexibilité et performance qui permet d'avoir le meilleur des deux mondes. Mais ce compromis a fortement évolué dans le temps, comme on va le voir plus bas. Rendre l'éclairage programmable permet d'implémenter facilement un grand nombre d'effets graphiques sans avoir à les implémenter en hardware. Avant les ''shaders'', les effets graphiques derniers cri n'étaient disponibles que sur les derniers modèles de carte graphique. Avec des ''vertex/pixel shaders'', ce genre de défaut est passé à la trappe. Si un nouvel algorithme de rendu graphique est inventé, il peut être utilisé dès le lendemain sur toutes les cartes graphiques modernes. De plus, implémenter beaucoup d'algorithmes d'éclairage différents avec des circuits fixes a un cout en termes de transistors, alors qu'utiliser des circuits programmable a un cout en hardware plus limité. Tout cela est à l'exact opposé de ce qu'on a avec les autres circuits, comme les circuits pour la rastérisation ou le placage de texture. Il n'y a pas 36 façons de rastériser une scène 3D et la flexibilité n'est pas un besoin important pour cette opération, alors que les performances sont cruciales. Même chose pour le placage/filtrage de textures. En conséquences, les unités de rastérisation, de texture, et les ROPs sont toutes implémentées en matériel. Faire ainsi permet de gagner en performance sans que cela ait le moindre impact pour le programmeur. Reste à expliquer dans le détail pourquoi. ===Les unités de texture sont intégrées aux processeurs de shaders=== Avec l'arrivée des processeurs de shaders, les unités de texture ont été intégrées dans les processeurs de shaders eux-mêmes. C'est la seule unité fixe qui a subit ce traitement, et il est intéressant de comprendre pourquoi. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=2|Architecture de base d'une carte 3D.]] Pour cela, il faut faire un rappel sur ce qu'il y a dans un processeur. Un processeur contient globalement quatre circuits : * une unité de calcul qui fait des calculs ; * des registres pour stocker les opérandes et résultats des calculs ; * une unité de communication avec la mémoire ; * et un séquenceur, un circuit de contrôle qui commande les autres. L'unité de communication avec la mémoire sert à lire ou écrire des données, à les transférer de la RAM vers les registres, ou l'inverse. Lire une donnée demande d'envoyer son adresse à la RAM, qui répond en envoyant la donnée lue. Elle est donc toute indiquée pour lire une texture : lire une texture n'est qu'un cas particulier de lecture de données. Les texels à lire sont à une adresse précise, la RAM répond à la lecture avec le texel demandé. Il est donc possible d'utiliser l'unité de communication avec la mémoire comme si c'était une unité de texture. Cependant, les textures ne sont pas utilisées comme telles de nos jours. Le rendu 3D moderne utilise des techniques dites de filtrage de texture, qui permettent d'améliorer la qualité du rendu des textures. Sans ce filtrage de texture, les textures appliquées naïvement donnent un résultat assez pixelisé et assez moche, pour des raisons assez techniques. Le filtrage élimine ces artefacts, en utilisant une forme d'''antialiasing'' interne aux textures, le fameux filtrage de texture. Le filtrage de texture peut être réalisé en logiciel ou en matériel. Techniquement, il est possible de le faire dans un ''shader''. Le ''shader'' calcule les adresses des texels à lire, lit les texels, et effectue ensuite le filtrage avec des opérations de calcul. Mais ce n'est pas ce qui est fait, le filtrage de texture est toujours effectué directement en matériel. La raison est que le filtrage de texture est très simple à implémenter en hardware. Le filtrage bilinéaire ou trilinéaire demande juste des circuits d'interpolation et quelques registres, ce qui est trivial. Et la seconde raison est qu'il n'y a pas 36 façons de filtrer des textures : une carte graphique peut implémenter les algorithmes principaux existants en assez peu de circuits. Pour simplifier l'implémentation, les processeurs de ''shader'' modernes disposent d'une unité d'accès mémoire séparée de l'unité de texture. L'unité d'accès mémoire normale s'occupe des accès mémoire hors-textures, alors que l'unité mémoire s'occupe de lire les textures. L'unité de texture contient de quoi faire du filtrage de texture, mais aussi faire des calculs d'adresse spécialisées, intrinsèquement liés au format des textures, qu'on détaillera dans le chapitre sur les textures. En comparaison, les unités d'accès mémoire effectuent des calculs d'adresse plus basiques. Un dernier avantage est que l'unité de texture est reliée au cache de texture, alors que l'unité d'accès mémoire est relié au cache L1/L2. ===Les ROPs peuvent être implémentés dans le ''pixel shader''=== Les ROPs effectuent plusieurs opérations basiques, mais les deux plus importantes sont la gestion du tampon de profondeur et de la transparence. Par transparence, on veut parler du mélange ''alpha''. Pour la gestion du tampon de profondeur, on veut parler du ''z-test'', qui compare la profondeur de deux pixels/fragments. Il s'agit d'opérations simples, qu'un processeur de shader peut faire sans problèmes. Par exemple, le ''z-test'' demande de faire plusieurs étapes : * calculer l'adresse du pixel dans le tampon de profondeur ; * lire le pixel dans le tampon de profondeur ; * Faire la comparaison entre profondeurs ; * Si le résultat de la comparaison est okay : ** écrire la nouvelle valeur z dans le tampon de profondeur, et écrire le nouveau pixel dedans. Le mélange ''alpha'' demande lui de : * calculer l'adresse du pixel dans le ''framebuffer'' ; * lire le pixel dans le ''framebuffer'' ; * faire des additions et multiplications pour le mélange ''alpha'' : * écrire le nouveau pixel dans le ''framebuffer''. Pour résumer il faut pouvoir faire : calcul d'adresse, lecture, écriture, addition, multiplication et comparaisons. Et toutes ces opérations sont supportées nativement par les processeurs de shaders, ce sont des instructions communes. Il est donc possible d'émuler les ROPs dans les pixels shaders. En pratique, c'est assez rare, et il y a une bonne explication à cela. Émuler les ROPs dans un ''pixel shader'' est trivial, comme on vient de le voir. Sauf que cela ne marche que si le GPU fait le rendu un pixel à la fois. Le tampon de profondeur est conçu pour traiter un pixel à la fois, idem pour le mélange ''alpha''. Mais si on ne traite pas l'image pixel par pixel, alors les deux algorithmes dysfonctionnent. Donc, tout va bien s'il n'y a qu'un seul processeur de ''pixel shader'', et que celui-ci est conçu pour ne traiter qu'un pixel à la fois, qu'une seule instance de ''shader''. Mais cela ne marche pas sur les GPU modernes, qui ont non seulement près d'une centaine de processeurs de shaders. Pour donner un exemple, imaginons la situation illustrée ci-dessous. Supposons que l'on ait assez de processeurs de shaders pour traiter plusieurs triangles en même temps. Par malchance, les processeurs rendent en même temps deux triangles opaques qui se recouvrent à l'écran. Là où ils se recouvrent, les deux triangles vont générer deux fragments par pixel, et un seul sera le bon. Pas de chance, les deux fragments sont rendus en parallèle dans deux processeurs séparés. Les deux processeurs lisent la même donnée dans le tampon de profondeur, les fragments passent le ''z-test'', les deux processeurs vont alors écrire leur résultat en mémoire et c'est premier arrivé, premier servi. Le résultat n'est pas forcément celui attendu : le pixel le plus proche peut être écrit avant le plus lointain, ou inversement. [[File:Situation où faire le z-test dans les pixel shaders dysfonctionne.png|centre|vignette|upright=2|Situation où faire le z-test dans les pixel shaders dysfonctionne]] Pour obtenir un bon rendu, le GPU doit forcer le z-test à se faire fragment par fragment, pour chaque pixel. Il ne doit pas être possible d'avoir deux fragments qui modifient le même pixel, dans deux processeurs de shaders séparés. Et pour garantir cela, il n'y a pas beaucoup de solutions. Une solution possible serait de mémoriser, pour chaque processeur de shader, les coordonnées du pixel qu'il traite à l'écran. Quand le rastériseur génère un fragment, il vérifie s'il y a conflit avec les fragments en cours de traitement, et attend si c'est le cas. Ou du moins, s'il fait des tests en parallèles, il doit faire en sorte que le résultat soit corrigé d'une manière ou d'une autre. En utilisant des processeurs de shaders qui travaillent en parallèle, cette contrainte est parfois brisée et le rendu donne des résultats incorrects. Le tampon de profondeur n'est pas conçu pour être parallélisé, idem pour le mélange ''alpha''. Il faut donc une sorte de point de synchronisation dans le pipeline pour éviter tout problème. Et c'est à ça que servent les ROPs. Les cartes graphiques pour PC ont des ROPs séparés des processeurs de ''shaders''. Par contre, quelques cartes graphiques destinées les smartphones et autres appareils mobiles font exception. Sur celles-ci, les unités de textures et les ROPs sont intégrés dans les processeurs de ''shaders'', aux côtés de l'unité d'accès mémoire LOAD/STORE. Il s'agit de cartes graphiques en rendu à tuiles. {{NavChapitre | book=Les cartes graphiques | prev=Les cartes graphiques : architecture de base | prevText=Les cartes graphiques : architecture de base | next=Les processeurs de shaders | nextText=Les processeurs de shaders }} {{autocat}} m6tt0zxlpntbosoo4uzyxp63jg0ovyh 763508 763507 2026-04-11T20:44:12Z Mewtow 31375 /* Les ROPs peuvent être implémentés dans le pixel shader */ 763508 wikitext text/x-wiki Il est intéressant d'étudier le hardware des cartes graphiques en faisant un petit résumé de leur évolution dans le temps. En effet, leur hardware a fortement évolué dans le temps. Et il serait difficile à comprendre le hardware actuel sans parler du hardware d'antan. En effet, une carte graphique moderne est partiellement programmable. Certains circuits sont totalement programmables, d'autres non. Et pour comprendre pourquoi, il faut étudier comment ces circuits ont évolués. Le hardware des cartes graphiques a fortement évolué dans le temps, ce qui n'est pas une surprise. Les évolutions de la technologie, avec la miniaturisation des transistors et l'augmentation de leurs performances a permis aux cartes graphiques d'incorporer de plus en plus de circuits avec les années. Avant l'invention des cartes graphiques, toutes les étapes du pipeline graphique étaient réalisées par le processeur : il calculait l'image à afficher, et l’envoyait à une carte d'affichage 2D. Au fil du temps, de nombreux circuits furent ajoutés, afin de déporter un maximum de calculs vers la carte vidéo. Le rendu 3D moderne est basé sur le placage de texture inverse, avec des coordonnées de texture, une correction de perspective, etc. Mais les anciennes consoles et bornes d'arcade utilisaient le placage de texture direct. Et cela a impacté le hardware des consoles/PCs de l'époque. Avec le placage de texture direct, il était primordial de calculer la géométrie, mais la rasterisation était le fait de VDC améliorés. Aussi, les premières bornes d'arcade 3D et les consoles de 5ème génération disposaient processeurs pour calculer la géométrie et de circuits d'application de textures très particuliers. A l'inverse, les PC utilisaient un rendu inverse, totalement différent. Sur les PC, les premières cartes graphiques avaient un circuit de rastérisation et des unités de textures, mais pas de circuits géométriques. ==Les premières cartes graphiques== Dès les années 70-80, le rendu 3D était utilisé par de nombreuses entreprises industrielles : des applications de visualisation 3D étaient utilisées en architecture, des applications de conception assistée par ordinateur étaient déjà d'utilisation courante, sans compter les simulateurs de vol utilisés par l'armée et les instructeurs qui formaient les pilotes d'avion. Le rendu 3D était aussi étudié au niveau académique, la recherche en 3D était déjà florissante. Il existait même du matériel spécifiquement conçu pour le rendu graphique, mais celui-ci était spécifiquement dédié à des super-calculateurs ou des ''workstations'' (des sortes d'ancêtres des PC, très puissants pour l'époque, mais conçus uniquement pour les entreprises). ===Le début des années 80 : le rendu en fils de fer=== Le tout premier système de ce genre était le '''''Line Drawing System-1''''' de l'entreprise Evans & Sutherland, daté de 1969. Ce n'est ni plus ni moins que le toute premier circuit graphique séparé du processeur ayant existé. C'est en un sens la toute première carte graphique, le tout premier GPU. Il prenait la forme d'un périphérique qui se connectait à l'ordinateur d'un côté et était relié à l'écran de l'autre. Il était compatible avec un grand nombre d'ordinateurs et de processeurs existants. Il a été suivi par plusieurs successeurs, nommés ''Picture System 1, 2'' et le ''PS300 series''. [[File:Evans & Sutherland LDS-1 (1).jpg|vignette|Evans & Sutherland LDS-1 (1)]] Ils permettaient de faire du rendu en fil de fer, sans texture ni même sans polygones colorés. Un tel rendu était utile pour des applications assez limitées : architecture, dessin de molécules pour les entreprises pharmaceutique et certains centres de recherche, l'aérospatiale, etc. Ces cartes graphiques étaient utilisées de concert avec des écrans appelés '''écrans vectoriels''' (''vector display''). Pour simplifier, ils ressemblaient à des écrans CRT, sauf que le faisceau d'électron ne balayait pas l'écran ligne par ligne, mais traçait des lignes arbitraires à l'écran. On lui précisait deux points de coordonnées x1,y1 ; et x2,y2 ; puis l'écran tracait une ligne entre ces deux points. En général, la ligne tracée était maintenue pendant un long moment, entre plusieurs secondes et plusieurs minutes. L'intérieur du circuit était assez simple : un circuit de multiplication de matrice pour les calculs géométriques, un rastériser simplifié (le ''clipping diviser''), un circuit de tracé de lignes, et un processeur de contrôle pour commander les autres circuits. Le fait que ces trois circuits soient séparés permettait une implémentation en pipeline, où plusieurs portions de l'image pouvaient être calculées en même temps : pendant que l'une est dans l'unité géométrique, l'autre est dans le rastériseur et une troisième est en cours de tracé. [[File:Lds1blockdiagram05.svg|centre|vignette|upright=2|Architecture du LDS-1. Le processeur de contrôle n'est pas représenté.]] Le processeur de contrôle exécute un programme qui se charge de commander l'unité géométrique et les autres circuits. Le programme en question est fourni par le programmeur, le LDS-1 est donc totalement programmable. Il lit directement les données nécessaires pour le rendu dans la mémoire de l’ordinateur et le programme exécuté est lui aussi en mémoire principale. Il n'a pas de mémoire vidéo dédiée, il utilise la RAM de l'ordinateur principal. Le multiplieur de matrices est plus complexe qu'on pourrait s'y attendre. Il ne s'agit pas que d'un circuit arithmétique tout simple, mais d'un véritable processeur avec des registres et des instructions machine complexes. Il contient plusieurs registres, l'ensemble mémorisant 4 matrices de 16 nombres chacune (4 lignes de 4 colonnes). Un nombre est codé sur 18 bits. Les registres sont reliés à un ensemble de circuits arithmétiques, des additionneurs et des multiplieurs. Le circuit supporte des instructions de copie entre registres, pour copier une ligne d'une matrice à une autre, des instructions LOAD/STORE pour lire ou écrire dans la mémoire RAM, etc. Il supporte aussi des multiplications en 2D et 3D. Le ''clipping divider'' est un circuit assez complexe, contenant un processeur à accumulateur, une mémoire ROM pour le programme du processeur. Le programme exécuté par le processeur est un petit programme de 62 instructions, stocké dans la ROM. L'algorithme du ''clipping divider'' est décrite dans le papier de recherche "A clipping divider", écrit par Robert Sproull. Un détail assez intéressant est que le résultat en sortie de l'unité géométrique et du rastériseur peuvent être envoyés à l'ordinateur en parallèle du rendu. C'était très utile sur les anciens ordinateurs qui étaient connectés à plusieurs terminaux. Le LDS-1 calculait la géométrie et le rendu, et le tout pouvait petre envoyé à d'autres composants, comme des terminaux, une imprimante, etc. ===Les systèmes ultérieurs : rendu à triangles colorés et texturé=== Les systèmes précédents étaient très limités : ils calculaient la géométrie et n'avaient pas de ''framebuffer'', ni de tampon de profondeur, ni gestion de l'éclairage, ni quoique ce soit. De tels systèmes étaient donc des accélérateurs géométriques que de vrais systèmes graphiques complets, du fait de l'absence de ''framebuffer''. Ils étaient composés de processeurs spécialisés dans les calculs à virgule flottante, faisant des calculs géométriques, et éventuellement d'un processeur pour la rastérisation. La raison est que la RAM était très chère et que créer des circuits fixes étaient très chers et peu disponibles. Par contre, les processeurs à virgule flottante étaient peu chers et facile à trouver. Vers la fin des années 80, grâce à la baisse du prix de la RAM et la démocratisation des ASIC (des circuits fixes fait sur mesure), ajouter un ''framebuffer'' est est devenu possible. C'est alors que sont apparus les '''systèmes de rendu 3D de première génération'''. De tels systèmes ont permis d'implémenter le rendu à primitives colorées qu'on a vu il y a quelques chapitres, à savoir un rendu où les triangles sont coloriés avec une couleur unique. Les systèmes de première génération étaient simples : des processeurs pour le calcul de la géométrie, un circuit de rastérisation, une RAM pour le ''framebuffer'' et des ASIC servant de ROPs très simples. Il n'y avait pas d'élimination des pixels cachés, pas de textures, et encore moins d'éclairage par pixels. Le premier système de ce genre était le ''Shaded Picture System'', toujours par Evans & Sutherland. Il ne gérait pas la couleur et ne pouvait afficher que des images en noir et blanc, mais il gérait l'éclairage par sommet (''vertex lighting''). Il a rapidement été dépassé par les systèmes de l'entreprise ''Silicon Graphics Inc'' (SGI), ainsi que ceux de l'entreprise Apollo avec sa série Apollo DN. Les '''systèmes de seconde génération''' sont apparus vers la fin des années 80, et se distinguent des précédents par l'ajout un tampon de profondeur. Ils intègrent aussi des capacités d'éclairage par pixel, à savoir de l'éclairage plat, de Gouraud, voire de Phong ! Enfin, les '''systèmes de troisième génération''' ont acquis des capacités de placage de texture, que les systèmes précédents n'avaient pas. Ils ont aussi ajouté un support de l'antialiasing. Les systèmes SGI avec placage de texture ont déjà été abordé au chapitre précédent, dans la section sur les GPU en mode immédiat et à ''tile''. Aussi, nous ne reviendrons pas dessus. [[File:Evolution de l'architecture des premières cartes graphiques, dans les années 80-90.png|centre|vignette|upright=2.5|Evolution de l'architecture des premières cartes graphiques, dans les années 80-90]] Les systèmes de première, seconde et troisième génération avaient de nombreux points communs. En premier lieu, ils étaient fabriqués en connectant plusieurs cartes électroniques : une carte pour les calculs géométriques, une ou plusieurs cartes pour le reste du rendu graphique, une carte dédiée au VDC et avec un connecteur écran. Les transistors de l'époque n'étaient pas encore miniaturisés, ce qui fait que le système graphique ne pouvait pas tenir sur une seule carte électronique. Il n'y avait donc pas de carte graphique proprement dit, mais un équivalent éclaté sur plusieurs cartes électroniques. La carte pour la géométrie contenait typiquement une mémoire FIFO pour accumuler les commandes de rendu, un processeur de commande, et plusieurs processeurs géométriques. Les processeurs géométriques étaient parfois conçus sur mesure, comme l'a été le le ''Geometry Engine'' de SGI. Mais il est arrivé qu'ils utilisent des processeurs commerciaux comme le Weitek 3222, l'Intel i860, etc. Les processeurs pouvaient être placés en série ou en parallèle, comme expliqué dans le chapitre précédent. Le circuit de rastérisation était réalisé soit avec un processeur dédié, soit avec un circuit fixe, soit un mélange des deux. La rastérisation est en effet réalisée en plusieurs étapes, certaines peuvent être implémentées avec un processeur et d'autres avec des circuits fixes. Un point important est qu'à l'époque, le rendu n'utilisait pas que des triangles, mais des polygones en général. Ce n'est que par la suite que le rendu s'est focalisé sur les triangles et les ''quads'' (quadrilatères). Il arrivait que le système graphique gérait partiellement des polygones concaves, voire convexes. Sur les systèmes SGI, les calculs géométriques se faisaient avec des polygones, que la rastérisation découpait en triangles, le reste du rendu se faisait avec des triangles. Les stations de travail Apollo DN 10000VS découpaient les polygones en trapézoïdes orientés à l'horizontale, alignés avec des ''scanlines''. D'autres systèmes découpaient tout en triangle lors de l'étape géométrique ==Les précurseurs grand public : les bornes d'arcade== [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] L'accélération du rendu 3D sur les bornes d'arcade était déjà bien avancé dès les années 90. Les bornes d'arcade ont toujours été un segment haut de gamme de l'industrie du jeu vidéo, aussi ce n'est pas étonnant. Le prix d'une borne d'arcade dépassait facilement les 10 000 dollars pour les plus chères et une bonne partie du prix était celui du matériel informatique. Le matériel était donc très puissant et débordait de mémoire RAM comparé aux consoles de jeu et aux PC. La plupart des bornes d'arcade utilisaient du matériel standardisé entre plusieurs bornes. A l'intérieur d'une borne d'arcade se trouve une '''carte de borne d'arcade''' qui est une carte mère avec un ou plusieurs processeurs, de la RAM, une carte graphique, un VDC et pas mal d'autres matériels. La carte est reliée aux périphériques de la borne : joysticks, écran, pédales, le dispositif pour insérer les pièces afin de payer, le système sonore, etc. Le jeu utilisé pour la borne est placé dans une cartouche qui est insérée dans un connecteur spécialisé. Les cartes de bornes d'arcade étaient généralement assez complexes, elles avaient une grande taille et avaient plus de composants que les cartes mères de PC. Chaque carte contenait un grand nombre de chips pour la mémoire RAM et ROM, et il n'était pas rare d'avoir plusieurs processeurs sur une même carte. Et il n'était pas rare d'avoir trois à quatre cartes superposées dans une seule borne. Pour ceux qui veulent en savoir plus, Fabien Sanglard a publié gratuitement un livre sur le fonctionnement des cartes d'arcade CPS System, disponible via ce lien : [https://fabiensanglard.net/b/cpsb.pdf The book of CP System]. Les premières cartes graphiques des bornes d'arcade étaient des cartes graphiques 2D auxquelles on avait ajouté quelques fonctionnalités. Les sprites pouvaient être tournés, agrandit/réduits, ou déformés pour simuler de la perspective et faire de la fausse 3D. Par la suite, le vrai rendu 3D est apparu sur les bornes d'arcade. Dès 1988, la carte d'arcade Namco System 21 et Sega Model 1 géraient les calculs géométriques. Quelques années plus tard, les cartes graphiques se sont mises à supporter un éclairage de Gouraud et du placage de texture. Par exemple, le Namco System 22 et la Sega model 2 supportaient des textures 2D et comme le filtrage de texture (bilinéaire et trilinéaire), le mip-mapping, et quelques autres. Au passage, les cartes graphiques de la Namco System 22 étaient développées en partenariat avec Eans & Sutherland, qui avait commencé à se diversifier dans le marché grand public. Les cartes graphiques de l'époque faisaient les calculs géométriques sur plusieurs processeurs, généralement des processeurs de type DSP (des processeurs spécialisés dans le traitement de signal). Par exemple, la Namco System 2 utilisait 4 DSP de marque Texas Instruments TMS320C25, cadencés à 24,576 MHz. La carte d'arcade Sega Model 1 utilisait quant à elle un DSP spécialisé dans les calculs géométriques. Par la suite, les bornes d'arcade ont réutilisé le hardware des PC et autres consoles de jeux. ==La 3D sur les consoles de quatrième/cinquième génération== Les consoles avant la quatrième génération de console étaient des consoles purement 2D, sans circuits d'accélération 3D. Leur carte graphique était un simple VDC 2D, plus ou moins performant selon la console. Les premières consoles de jeu capables de rendu 3D par elles-mêmes sont les consoles dites de 5ème génération. Il y a diverses manières de classer les consoles en générations, la plus commune place la 3D à la 5ème génération, mais détailler ces controverses quant à ce classement nous amènerait trop loin. Les consoles de génération avaient une architecture assez différente des systèmes antérieurs. Les systèmes SGI et assimilés pouvaient se permettre de couter assez cher, d'utiliser beaucoup de circuits, de prendre beaucoup de place. Les bornes d'arcade sont aussi dans ce cas. Aussi, il n'était pas rare que les cartes 3D de l'époque tiennent sur plusieurs cartes électroniques séparées. Mais une console ne peut pas se permettre ce genre de folies. Aussi, les cartes 3D des consoles de l'époque tenaient dans un seul circuit intégré, comme il est d'usage de nos jours. La conséquence est que certains circuits étaient fortement simplifiés, sur les consoles de cinquième génération. Et cela a impacté l'architecture interne des GPU des consoles. Les systèmes SGI avaient plusieurs processeurs pour calculer la géométrie, couplés à plusieurs unités non-programmables pour les pixels/textures. Les cartes 3D des consoles gardaient cette organisation : processeurs pour la géométrie, circuits fixes pour le reste. Mais elles se débrouillaient souvent avec un seul processeur, voire aucun ! Dans ce dernier cas, la géométrie était calculée sur le processeur principal, le CPU. Les unités pour les pixels étaient aussi moins nombreuses, mais il y en avait plusieurs, pour profiter de l'amplification des pixels. : Les cartes 3D des consoles de jeu utilisaient le placage de texture inverse, avec quelques exceptions qui utilisaient le placage de texture direct. ===Le rendu 3D sur les consoles de quatrième génération : la SNES=== Plus haut, j'ai dit que les consoles de quatrième génération n'avaient pas de carte accélératrice 3D. Pourtant, elles ont connus quelques jeux en vraie 3D. La raison à cela est que la 3D était calculée par un GPU placé dans les cartouches du jeu ! Par exemple, les cartouches de Starfox et de Super Mario 2 contenaient un coprocesseur Super FX, qui gérait des calculs de rendu 2D/3D. En tout, il y a environ 16 coprocesseurs pour la SNES et on en trouve facilement la liste sur le net. La console était conçue pour, des pins sur les ports cartouches étaient prévues pour des fonctionnalités de cartouche annexes, dont ces coprocesseurs. Ces pins connectaient le coprocesseur au bus des entrées-sorties. Les coprocesseurs des cartouches de NES avaient souvent de la mémoire rien que pour eux, qui était intégrée dans la cartouche. Ceci étant dit, passons aux consoles de cinquième génération. ===La Nintendo 64 : un GPU avancé=== La Nintendo 64 avait le GPU le plus complexe comparé aux autres consoles, et dépassait même les cartes graphiques des PC. Il faut dire que son GPU a été conçu avec l'aide de l'entreprise SGI, dont on a vu les systèmes graphiques plus haut. Le GPU de la N64 incorporait une unité pour les calculs géométriques, un circuit de rasterisation, une unité de textures et un ROP final pour les calculs de transparence/brouillard/antialiasing, ainsi qu'un circuit pour gérer la profondeur des pixels. En somme, tout le pipeline graphique était implémenté dans le GPU de la Nintendo 64, chose très en avance sur son temps, comparé au PC ou aux autres consoles ! Le GPU est construit autour d'un processeur dédié aux calculs géométriques, le ''Reality Signal Processor'' (RSP), autour duquel on a ajouté des circuits pour le reste du pipeline graphique. L'unité de calcul géométrique est un processeur MIPS R4000, un processeur assez courant à l'époque, auquel on avait retiré quelques fonctionnalités inutiles pour le rendu 3D. Il était couplé à 4 KB de mémoire vidéo, ainsi qu'à 4 KB de mémoire ROM. Le reste du GPU était réalisé avec des circuits fixes. Un point intéressant est que le programme exécuté par le RSP pouvait être programmé ! Le RSP gérait déjà des espèces de proto-shaders, qui étaient appelés des ''[https://ultra64.ca/files/documentation/online-manuals/functions_reference_manual_2.0i/ucode/microcode.html micro-codes]'' dans la documentation de l'époque. La ROM associée au RSP mémorise cinq à sept programmes différents, aux fonctionnalités différentes. * Les microcodes gspFast3D et gspF3DNoN, implémentent un rendu 3D normal, avec des options de ''clipping'' différentes entre les deux. * Le microcode gspTurbo3D fait la même chose, mais avec moins de fonctionnalités et avec une précision réduite. Il ne gère pas le ''clipping'', l'éclairage par pixel, la correction de perspective, l'antialiasing et quelques autres fonctionnalités. Il gère cependant l'éclairage de Gouraud. Il utilise une ''display list'' simplifiée comparé aux deux microcodes précédents. * Le microcode gspZ-Sort effectue une pré-passe z, à savoir qu'il calcule le tampon de profondeur final de la scène 3D, sans rendre l'image. Cela sert à faire une élimination des pixels cachés parfaite, en logiciel. On calcule le tampon de profondeur pour déterminer quels pixels sont visibles, puis une seconde passe rend l'image en, rejetant les pixels non-visibles. * Le microcode gspSprite2D implémente un rendu 2D émulé : les sprites et arrière-plan sont des rectangles texturés. Le microcode gspS2DEX fait la même chose, mais sert à émuler le rendu de la SNES plus qu'autre chose. * Le microcode gspLine3D ne gére que des lignes, pas de triangles. Il sert pour du rendu en fil de fer. Ils géraient le rendu 3D de manière différente et avec une gestion des ressources différentes. Très peu de studios de jeu vidéo ont développé leur propre microcodes N64, car la documentation était mal faite, que Nintendo ne fournissait pas de support officiel pour cela, que les outils de développement ne permettaient pas de faire cela proprement et efficacement. ===La Playstation 1=== Sur la Playstation 1 le calcul de la géométrie était réalisé par le processeur, la carte graphique gérait tout le reste. Et la carte graphique était un circuit fixe spécialisé dans la rasterisation et le placage de textures. Elle utilisait, comme la Nintendo 64, le placage de texture inverse, qui est apparu ensuite sur les cartes graphiques. ===La 3DO et la Sega Saturn=== La Sega Saturn et la 3DO étaient les deux seules consoles à utiliser le rendu direct. La géométrie était calculée sur le processeur, même si les consoles utilisaient parfois un CPU dédié au calcul de la géométrie. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. La Sega Saturn incorpore trois processeurs et deux GPU. Les deux GPUs sont nommés le VDP1 et le VDP2. Le VDP1 s'occupe des textures et des sprites, le VDP2 s'occupe uniquement de l'arrière-plan et incorpore un VDC tout ce qu'il y a de plus simple. Ils ne gèrent pas du tout la géométrie, qui est calculée par les trois processeurs. Le troisième processeur, la Saturn Control Unit, est un processeur de type DSP, à savoir un processeur spécialisé dans le traitement de signal. Il est utilisé presque exclusivement pour accélérer les calculs géométriques. Il avait sa propre mémoire RAM dédiée, 32 KB de SRAM, soit une mémoire locale très rapide. Les transferts entre cette RAM et le reste de l'ordinateur était géré par un contrôleur DMA intégré dans le DSP. En somme, il s'agit d'une sorte de processeur spécialisé dans la géométrie, une sorte d'unité géométrique programmable. Mais la géométrie n'était pas forcément calculée que sur ce DSP, mais pouvait être prise en charge par les 3 CPU. ==L'historique des cartes graphiques des PC, avant l'arrivée de Direct X et Open Gl== Sur PC, l'évolution des cartes graphiques a eu du retard par rapport aux consoles. Les PC sont en effet des machines multi-usage, pour lesquelles le jeu vidéo était un cas d'utilisation parmi tant d'autres. Et les consoles étaient la plateforme principale pour jouer à des jeux vidéo, le jeu vidéo PC étant plus marginal. Mais cela ne veut pas dire que le jeu PC n'existait pas, loin de là ! Un problème pour les jeux PC était que l'écosystème des PC était aussi fragmenté en plusieurs machines différentes : machines Apple 1 et 2, ordinateurs Commdore et Amiga, IBM PC et dérivés, etc. Aussi, programmer des jeux PC n'était pas mince affaire, car les problèmes de compatibilité étaient légion. C'est seulement quand la plateforme x86 des IBM PC s'est démocratisée que l'informatique grand public s'est standardisée, réduisant fortement les problèmes de compatibilité. Mais cela n'a pas suffit, il a aussi fallu que les API 3D naissent. Les API 3D comme Direct X et Open GL sont absolument cruciales pour garantir la compatibilité entre plusieurs ordinateurs aux cartes graphiques différentes. Aussi, l'évolution des cartes graphiques pour PC s'est faite main dans la main avec l'évolution des API 3D. Les fonctionnalités des cartes graphiques ont évolué dans le temps, en suivant les évolutions des API 3D. Du moins dans les grandes lignes, car il est arrivé plusieurs fois que des fonctionnalités naissent sur les cartes graphiques, pour que les fabricants forcent la main de Microsoft ou d'Open GL pour les intégrer de force dans les API 3D. Passons. ===L'introduction des premiers jeux 3D : Quake et les drivers miniGL=== L'API OpenGL est née de la main de SGI, encore eux ! SGI avait créé l'API Iris GL pour ses stations de travail Iris Graphics. Iris GL a ensuite été libéré et est devenu le standard Open GL. Open GL existait déjà avant l'apparition des cartes accélératrices 3D. Il y a avait donc déjà un terreau que les programmeurs graphiques pouvaient utiliser. Mais Open GL était surtout utilisé pour des applications industrielles, médicales (imagerie), graphiques ou militaires, pas pour le jeu vidéo. Mais cela changea avec la sortie du jeu Quake, d'IdSoftware, en 1996. Quake pouvait fonctionner en rendu logiciel, mais le programmeur responsable du moteur 3D (le célébre John Carmack) ajouta une version OpenGL du jeu. Il faut dire que le jeu était programmé sur une station de travail compatible avec OpenGL, même si aucune carte accélératrice de l'époque ne supportait OpenGL. C'était là un choix qui se révéla visionnaire. En théorie, le rendu par OpenGL aurait dû se faire intégralement en logiciel, sauf sur quelques rares stations de travail adaptées. Mais les premières cartes graphiques étaient déjà dans les starting blocks. La toute première carte 3D pour PC est la '''Rendition Vérité V1000''', sortie en Septembre 1995, soit quelques mois avant l'arrivée de la Nintendo 64. La Rendition Vérité V1000 contenait un processeur MIPS cadencé à 25 MHz, 4 mébioctets de RAM, une ROM pour le BIOS, et un RAMDAC, rien de plus. C'était un vrai ordinateur complètement programmable de bout en bout, sans aucun circuit fixe. Les programmeurs ne pouvaient cependant pas utiliser cette programmabilité avec des ''shaders'', mais elle permettait à Rendition d'implémenter n'importe quelle API 3D, que ce soit OpenGL, DirectX ou même sa son API propriétaire. La Rendition Vérité avait de bonnes performances pour ce qui est de la géométrie, mais pas pour le reste. Réaliser la rastérisation et le placage de texture en logiciel n'est pas efficace, pareil pour les opérations de fin de pipeline comme l'antialiasing. Le manque d'unités fixes très rapides pour la rastérisation, le placage de texture ou les opérations de fin de pipeline était clairement un gros défaut. Mais la Rendition Vérité était un cas à part, une exception dans le paysage des cartes 3D de l'époque, qui ne faisait rien comme les autres. Les autres cartes graphiques, sorties peu après, étaient les Voodoo de 3dfx, les Riva TNT de NVIDIA, les Rage/3D d'ATI, la Virge/3D de S3, et la Matrox Mystique. Elles avaient choisit le compromis inverse de la Rendition Vérité V1000 : de bonnes performances pour le placage de textures et la rastérization, mais pas pour les calculs géométriques. Pour rappel, les systèmes professionnels et les consoles avaient des processeurs pour la géométrie, et des circuits fixes pour le reste. Les cartes graphiques de PC se passaient des processeurs pour la géométrie, les calculs géométriques étaient réalisés par le CPU. Les toutes premières cartes 3D pour PC contenaient seulement des circuits pour gérer les textures et des ROPs. Elle géraient le ''z-buffer'' en mémoire vidéo, ainsi que des effets de brouillard. Il n'y avait même pas de circuit pour la rastérisation, qui était faite en logiciel, avec les calculs géométriques. [[File:Architecture de base d'une carte 3D - 2.png|centre|vignette|upright=1.5|Carte 3D sans rasterization matérielle.]] Les cartes suivantes ajoutèrent une gestion des étapes de ''rasterization'' directement en matériel. Les cartes ATI rage 2, les Invention de chez Rendition, et d'autres cartes graphiques supportaient la rasterisation en hardware. [[File:Architecture de base d'une carte 3D - 3.png|centre|vignette|upright=1.5|Carte 3D avec gestion de la géométrie.]] Pour exploiter les unités de texture et le circuit de rastérisation, OpenGL et Direct 3D étaient partiellement implémentées en logiciel, car les cartes graphiques ne supportaient pas toutes les fonctionnalités de l'API. C'était l'époque du miniGL, des implémentations partielles d'OpenGL, fournies par les fabricants de cartes 3D, implémentées dans les pilotes de périphériques de ces dernières. Les fonctionnalités d'OpenGL implémentées dans ces pilotes étaient presque toutes exécutées en matériel, par la carte graphique. Avec l'évolution du matériel, les pilotes de périphériques devinrent de plus en plus complets, au point de devenir des implémentations totales d'OpenGL. Mais au-delà d'OpenGL, chaque fabricant de carte graphique avait sa propre API propriétaire, qui était gérée par leurs pilotes de périphériques (''drivers''). Par exemple, les premières cartes graphiques de 3dfx interactive, les fameuses voodoo, disposaient de leur propre API graphique, l'API Glide. Elle facilitait la gestion de la géométrie et des textures, ce qui collait bien avec l'architecture de ces cartes 3D. Mais ces API propriétaires tombèrent rapidement en désuétude avec l'évolution de DirectX et d'OpenGL. Direct X était une API dans l'ombre d'Open GL. La première version de Direct X qui supportait la 3D était DirectX 2.0 (juin 2, 1996), suivie rapidement par DirectX 3.0 (septembre 1996). Elles dataient d'avant le jeu Quake, et elles étaient très éloignées du hardware des premières cartes graphiques. Elles utilisaient un système d'''execute buffer'' pour communiquer avec la carte graphique, Microsoft espérait que le matériel 3D implémenterait ce genre de système. Ce qui ne fu pas le cas. Direct X 4.0 a été abandonné en cours de développement pour laisser à une version 5.0 assez semblable à la 2.0/3.0. Le mode de rendu laissait de côté les ''execute buffer'' pour coller un peu plus au hardware de l'époque. Mais rien de vraiment probant comparé à Open GL. Même Windows utilisait Open GL au lieu de Direct X maison... C'est avec Direct X 6.0 que Direct X est entré dans la cours des grands. Il gérait la plupart des technologies supportées par les cartes graphiques de l'époque. ===Le ''multi-texturing'' de l'époque Direct X 6.0 : combiner plusieurs textures=== Une technologie très importante standardisée par Dirext X 6 est la technique du '''''multi-texturing'''''. Avec ce qu'on a dit dans le chapitre précédent, vous pensez sans doute qu'il n'y a qu'une seule texture par objet, qui est plaquée sur sa surface. Mais divers effet graphiques demandent d'ajouter des textures par dessus d'autres textures. En général, elles servent pour ajouter des détails, du relief, sur une surface pré-existante. Un exemple intéressant vient des jeux de tir : ajouter des impacts de balles sur les murs. Pour cela, on plaque une texture d'impact de balle sur le mur, à la position du tir. Il s'agit là d'un exemple de '''''decals''''', des petites textures ajoutées sur les murs ou le sol, afin de simuler de la poussière, des impacts de balle, des craquelures, des fissures, des trous, etc. Les textures en question sont de petite taille et se superposent à une texture existante, plus grande. Rendre des ''decals'' demande de pouvoir superposer deux textures. Direct X 6.0 supportait l'application de plusieurs textures directement dans le matériel. La carte graphique devait être capable d'accéder à deux textures en même temps, ou du moins faire semblant que. Pour cela, elle doublaient les unités de texture et adaptaient les connexions entre unités de texture et mémoire vidéo. La mémoire vidéo devait être capable de gérer plusieurs accès mémoire en même temps et devait alors avoir un débit binaire élevé. [[File:Multitexturing.png|centre|vignette|upright=2|Multitexturing]] La carte graphique devait aussi gérer de quoi combiner deux textures entre elles. Par exemple, pour revenir sur l'exemple d'une texture d'impact de balle, il faut que la texture d'impact recouvre totalement la texture du mur. Dans ce cas, la combinaison est simple : la première texture remplace l'ancienne, là où elle est appliquée. Mais les cartes graphiques ont ajouté d'autres combinaisons possibles, par exemple additionner les deux textures entre elle, faire une moyenne des texels, etc. Les opérations pour combiner les textures était le fait de circuits appelés des '''''combiners'''''. Concrètement, les ''combiners'' sont de simples unités de calcul. Les ''conbiners'' ont beaucoup évolués dans le temps, mais les premières implémentation se limitaient à quelques opérations simples : addition, multiplication, superposition, interpolation. L'opération effectuer était envoyée au ''conbiner'' sur une entrée dédiée. [[File:Multitexturing avec combiners.png|centre|vignette|upright=2|Multitexturing avec combiners]] S'il y avait eu un seul ''conbiner'', le circuit de ''multitexturing'' aurait été simplement configurable. Mais dans la réalité, les premières cartes utilisant du ''multi-texturing'' utilisaient plusieurs ''combiners'' placés les uns à la suite des autres. L'implémentation des ''combiners'' retenue par Open Gl, et par le hardware des cartes graphiques, était la suivante. Les ''combiners'' étaient placés en série, l'un à la suite de l'autre, chacun combinant le résultat de l'étage précédent avec une texture. Le premier ''combiner'' gérait l'éclairage par sommet, afin de conserver un minimum de rétrocompatibilité. [[File:Texture combiners Open GL.png|centre|vignette|upright=2|Texture combiners Open GL]] Voici les opérations supportées par les ''combiners'' d'Open GL. Ils prennent en entrée le résultat de l'étage précédent et le combinent avec une texture lue depuis l'unité de texture. {|class="wikitable" |+ Opérations supportées par les ''combiners'' d'Open GL |- ! Replace | colspan="2" | Pixel provenant de l'unité de texture |- ! Addition | colspan="2" | Additionne l'entrée au texel lu. |- ! Modulate | colspan="2" | Multiplie l'entrée avec le texel lu |- ! Mélange (''blending'') | Moyenne pondérée des deux entrées, pondérée par la composante de transparence || La couleur de transparence du texel lu et de l'entrée sont multipliées. |- ! Decals | Moyenne pondérée des deux entrées, pondérée par la composante de transparence. || La transparence du résultat est celle de l'entrée. |} Il faut noter qu'un dernier étage de ''combiners'' s'occupait d'ajouter la couleur spéculaire et les effets de brouillards. Il était à part des autres et n'était pas configurable, c'était un étage fixe, qui était toujours présent, peu importe le nombre de textures utilisé. Il était parfois appelé le '''''combiner'' final''', terme que nous réutiliserons par la suite. Mine de rien, cela a rendu les cartes graphiques partiellement programmables. Le fait qu'il y ait des opérations enchainées à la suite, opérations qu'on peut choisir librement, suffit à créer une sorte de mini-programme qui décide comment mélanger plusieurs textures. Mais il y avait une limitation de taille : le fait que les données soient transmises d'un étage à l'autre, sans détours possibles. Par exemple, le troisième étage ne pouvait avoir comme seule opérande le résultat du second étage, mais ne pouvait pas utiliser celui du premier étage. Il n'y avait pas de registres pour stocker ce qui sortait de la rastérisation, ni pour mémoriser temporairement les texels lus. ===Le ''Transform & Lighting'' matériel de Direct X 7.0=== [[File:Architecture de base d'une carte 3D - 4.png|vignette|upright=1.5|Carte 3D avec gestion de la géométrie.]] La première carte graphique pour PC capable de gérer la géométrie en hardware fût la Geforce 256, la toute première Geforce. Son unité de gestion de la géométrie n'est autre que la bien connue '''unité T&L''' (''Transform And Lighting''). Elle implémentait des algorithmes d'éclairage de la scène 3D assez simples, comme un éclairage de Gouraud, qui étaient directement câblés dans ses circuits. Mais contrairement à la Nintendo 64 et aux bornes d'arcade, elle implémentait le tout, non pas avec un processeur classique, mais avec des circuits fixes. Avec Direct X 7.0 et Open GL 1.0, l'éclairage était en théorie limité à de l'éclairage par sommet, l'éclairage par pixel n'était pas implémentable en hardware. Les cartes graphiques ont tenté d'implémenter l'éclairage par pixel, mais cela n'est pas allé au-delà du support de quelques techniques de ''bump-mapping'' très limitées. Par exemple, Direct X 6.0 implémentait une forme limitée de ''bump-mapping'', guère plus. Un autre problème est qu'il a beaucoup d'algorithmes d'éclairages différents, aux résultats visuels différents, bien au-delà des algorithmes d'éclairage plat, de Gouraud et de Phong. Et les unités de T&L étaient souvent en retard sur les algorithmes logiciels. Les programmeurs avaient le choix entre programmer les algorithmes d’éclairage qu'ils voulaient et les exécuter en logiciel, ou utiliser ceux de l'unité de T&L. Ils choisissaient souvent la première option. Par exemple, Quake 3 Arena et Unreal Tournament n'utilisaient pas les capacités d'éclairage géométrique et préféraient utiliser leurs calculs d'éclairage logiciel fait maison. Cependant, le hardware dépassait les capacités des API et avait déjà commencé à ajouter des capacités de programmation liées au ''multi-texturing''. Les cartes graphiques de l'époque, surtout chez NVIDIA, implémentaient un système de '''''register combiners''''', une forme améliorée de ''texture combiners'', qui permettait de faire une forme limitée d'éclairage par pixel, notamment du vrai ''bump-mampping'', voire du ''normal-mapping''. Mais ce n'était pas totalement supporté par les API 3D de l'époque. Les ''registers combiners'' sont des ''texture combiners'' mais dans lesquels ont aurait retiré la stricte organisation en série. Il y a toujours plusieurs étages à la suite, qui peuvent exécuter chacun une opération, mais tous les étages ont maintenant accès à toutes les textures lues et à tout ce qui sort de la rastérisation, pas seulement au résultat de l'étape précédente. Pour cela, on ajoute des registres pour mémoriser ce qui sort des unités de texture, et pour ce qui sort de la rastérisation. De plus, on ajoute des registres temporaires pour mémoriser les résultats de chaque ''combiner'', de chaque étage. Il faut cependant signaler qu'il existe un ''combiner'' final, séparé des étages qui effectuent des opérations proprement dits. Il s'agit de l'étage qui applique la couleur spéculaire et les effets de brouillards. Il ne peut être utilisé qu'à la toute fin du traitement, en tant que dernier étage, on ne peut pas mettre d'opérations après lui. Sa sortie est directement connectée aux ROPs, pas à des registres. Il faut donc faire la distinction entre les '''''combiners'' généraux''' qui effectuent une opération et mémorisent le résultat dans des registres, et le ''combiner'' final qui envoie le résultat aux ROPs. L'implémentation des ''register combiners'' utilisait un processeur spécialisés dans les traitements sur des pixels, une sorte de proto-processeur de ''shader''. Le processeur supportait des opérations assez complexes : multiplication, produit scalaire, additions. Il s'agissait d'un processeur de type VLIW, qui sera décrit dans quelques chapitres. Mais ce processeur avait des programmes très courts. Les premières cartes NVIDIA, comme les cartes TNT pouvaient exécuter deux opérations à la suite, suivie par l'application de la couleurs spéculaire et du brouillard. En somme, elles étaient limitées à un ''shader'' à deux/trois opérations, mais c'était un début. Le nombre d'opérations consécutives est rapidement passé à 8 sur la Geforce 3. ===L'arrivée des ''shaders'' avec Direct X 8.0=== [[File:Architecture de la Geforce 3.png|vignette|upright=1.5|Architecture de la Geforce 3]] Les ''register combiners'' était un premier pas vers un éclairage programmable. Paradoxalement, l'évolution suivante s'est faite non pas dans l'unité de rastérisation/texture, mais dans l'unité de traitement de la géométrie. La Geforce 3 a remplacé l'unité de T&L par un processeur capable d'exécuter des programmes. Les programmes en question complétaient l'unité de T&L, afin de pouvoir rajouter des techniques d'éclairage plus complexes. Le tout a permis aussi d'ajouter des animations, des effets de fourrures, des ombres par ''shadow volume'', des systèmes de particule évolués, et bien d'autres. À partir de la Geforce 3 de Nvidia, les cartes graphiques sont devenues capables d'exécuter des programmes appelés '''''shaders'''''. Le terme ''shader'' vient de ''shading'' : ombrage en anglais. Grace aux ''shaders'', l'éclairage est devenu programmable, il n'est plus géré par des unités d'éclairage fixes mais été laissé à la créativité des programmeurs. Les programmeurs ne sont plus vraiment limités par les algorithmes d'éclairage implémentés dans les cartes graphiques, mais peuvent implémenter les algorithmes d'éclairage qu'ils veulent et peuvent le faire exécuter directement sur la carte graphique. Les ''shaders'' sont classifiés suivant les données qu'ils manipulent : '''''pixel shader''''' pour ceux qui manipulent des pixels, '''''vertex shaders''''' pour ceux qui manipulent des sommets. Les premiers sont utilisés pour implémenter l'éclairage par pixel, les autres pour gérer tout ce qui a trait à la géométrie, pas seulement l'éclairage par sommets. Direct X 8.0 avait un standard pour les shaders, appelé ''shaders 1.0'', qui correspondait parfaitement à ce dont était capable la Geforce 3. Il standardisait les ''vertex shaders'' de la Geforce 3, mais il a aussi renommé les ''register combiners'' comme étant des ''pixel shaders'' version 1.0. Les ''register combiners'' n'ont pas évolués depuis la Geforce 256, si ce n'est que les programmes sont passés de deux opérations successives à 8, et qu'il y avait possibilité de lire 4 textures en ''multitexturing''. A l'opposé, le processeur de ''vertex shader'' de la Geforce 3 était capable d'exécuter des programmes de 128 opérations consécutives et avait 258 registres différents ! Des ''pixels shaders'' plus évolués sont arrivés avec l'ATI Radeon 8500 et ses dérivés. Elle incorporait la technologie ''SMARTSHADER'' qui remplacait les ''registers combiners'' par un processeur de ''shader'' un peu limité. Un point est que le processeur acceptait de calculer des adresses de texture dans le ''pixel shader''. Avant, les adresses des texels à lire étaient fournis par l'unité de rastérisation et basta. L'avantage est que certains effets graphiques étaient devenus possibles : du ''bump-mapping'' avancé, des textures procédurales, de l'éclairage par pixel anisotrope, du éclairage de Phong réel, etc. Avec la Radeon 8500, le ''pixel shader'' pouvait calculer des adresses, et lire les texels associés à ces adresses calculées. Les ''pixel shaders'' pouvaient lire 6 textures, faire 8 opérations sur les texels lus, puis lire 6 textures avec les adresses calculées à l'étape précédente, et refaire 8 opérations. Quelque chose de limité, donc, mais déjà plus pratique. Les ''pixel shaders'' de ce type ont été standardisé dans Direct X 8.1, sous le nom de ''pixel shaders 1.4''. Encore une fois, le hardware a forcé l'intégration dans une API 3D. ===Les ''shaders'' de Direct X 9.0 : de vrais ''pixel shaders''=== Avec Direct X 9.0, les ''shaders'' sont devenus de vrais programmes, sans les limitations des ''shaders'' précédents. Les ''pixels shaders'' sont passés à la version 2.0, idem pour les ''vertex shaders''. Concrètement, ils ont des fonctionnalités bien supérieures à celles des ''registers combiners''. Les ''shaders'' pouvaient exécuter une suite d'opérations arbitraire, dans le sens où elle n'était pas structurée avec tel type d'opération au début, suivie par un accès aux textures, etc. On pouvait mettre n'importe quelle opération dans n'importe quel ordre. De plus, les ''shaders'' ne sont plus écrit en assembleur comme c'était le cas avant. Ils sont dorénavant écrits dans un langage de haut-niveau, le HLSL pour les shaders Direct X et le GLSL pour les shaders Open Gl. Les ''shaders'' sont ensuite traduit (compilés) en instructions machines compréhensibles par la carte graphique. Au début, ces langages et la carte graphique supportaient uniquement des opérations simples. Mais au fil du temps, les spécifications de ces langages sont devenues de plus en plus riches à chaque version de Direct X ou d'Open Gl, et le matériel en a fait autant. Le matériel s'est alors adapté, en incorporant un véritable processeur pour les ''pixel shaders''. Les ''pixel shaders'' sont maintenant exécutés par un processeur de ''shader'' dédié, aux fonctionnalités bien supérieures à celles des ''registers combiners''. Le processeur de ''pixel shader'' incorpore l'unité de texture en sont sein, les deux sont fusionnés. La raison à cela sera expliqué dans la suite du chapitre. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=1.5|Carte 3D avec pixels et vertex shaders non-unifiés.]] ===L'après Direct X 9.0=== Avant Direct X 10, les processeurs de ''shaders'' ne géraient pas exactement les mêmes opérations pour les processeurs de ''vertex shader'' et de ''pixel shader''. Les processeurs de ''vertex shader'' et de ''pixel shader''étaient séparés. Depuis DirectX 10, ce n'est plus le cas : le jeu d'instructions a été unifié entre les vertex shaders et les pixels shaders, ce qui fait qu'il n'y a plus de distinction entre processeurs de vertex shaders et de pixels shaders, chaque processeur pouvant traiter indifféremment l'un ou l'autre. [[File:Architecture de base d'une carte 3D - 6.png|centre|vignette|upright=1.5|Architecture de la GeForce 6800.]] Avec Direct X 10, de nombreux autres ''shaders'' sont apparus. Les plus liés au rendu 3D sont les '''''geometry shader''''' pour ceux qui manipulent des triangles, de ''hull shaders'' et de ''domain shaders'' pour la tesselation. De plus, les cartes graphiques modernes sont capables d’exécuter des programmes informatiques qui n'ont aucun lien avec le rendu 3D, mais sont exécutés par la carte graphique comme le ferait un processeur d'ordinateur normal. De tels ''shaders'' sans lien avec le rendu 3D sont appelés des ''compute shader''. ==Les cartes graphiques d'aujourd'hui== Les circuits d'un GPU ont beaucoup évolué depuis l'introduction des ''shaders'', pour devenir de plus en plus programmables. Mais à côté des processeurs de ''shaders'', il reste quelques circuits non-programmables appelés des circuits fixes. La rastérisation, le placage de texture, l'élimination des pixels cachés et le mélange ''alpha'' sont gérés par des circuits fixes. [[File:3D-Pipeline.svg|centre|vignette|upright=3.0|Pipeline 3D : ce qui est programmable et ce qui ne l'est pas dans une carte graphique moderne.]] Mais pourquoi ne pas tout rendre programmable ? Ou au contraire, utiliser seulement des circuits fixes ? La réponse rapide est qu'il s'agit d'un compromis entre flexibilité et performance qui permet d'avoir le meilleur des deux mondes. Mais ce compromis a fortement évolué dans le temps, comme on va le voir plus bas. Rendre l'éclairage programmable permet d'implémenter facilement un grand nombre d'effets graphiques sans avoir à les implémenter en hardware. Avant les ''shaders'', les effets graphiques derniers cri n'étaient disponibles que sur les derniers modèles de carte graphique. Avec des ''vertex/pixel shaders'', ce genre de défaut est passé à la trappe. Si un nouvel algorithme de rendu graphique est inventé, il peut être utilisé dès le lendemain sur toutes les cartes graphiques modernes. De plus, implémenter beaucoup d'algorithmes d'éclairage différents avec des circuits fixes a un cout en termes de transistors, alors qu'utiliser des circuits programmable a un cout en hardware plus limité. Tout cela est à l'exact opposé de ce qu'on a avec les autres circuits, comme les circuits pour la rastérisation ou le placage de texture. Il n'y a pas 36 façons de rastériser une scène 3D et la flexibilité n'est pas un besoin important pour cette opération, alors que les performances sont cruciales. Même chose pour le placage/filtrage de textures. En conséquences, les unités de rastérisation, de texture, et les ROPs sont toutes implémentées en matériel. Faire ainsi permet de gagner en performance sans que cela ait le moindre impact pour le programmeur. Reste à expliquer dans le détail pourquoi. ===Les unités de texture sont intégrées aux processeurs de shaders=== Avec l'arrivée des processeurs de shaders, les unités de texture ont été intégrées dans les processeurs de shaders eux-mêmes. C'est la seule unité fixe qui a subit ce traitement, et il est intéressant de comprendre pourquoi. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=2|Architecture de base d'une carte 3D.]] Pour cela, il faut faire un rappel sur ce qu'il y a dans un processeur. Un processeur contient globalement quatre circuits : * une unité de calcul qui fait des calculs ; * des registres pour stocker les opérandes et résultats des calculs ; * une unité de communication avec la mémoire ; * et un séquenceur, un circuit de contrôle qui commande les autres. L'unité de communication avec la mémoire sert à lire ou écrire des données, à les transférer de la RAM vers les registres, ou l'inverse. Lire une donnée demande d'envoyer son adresse à la RAM, qui répond en envoyant la donnée lue. Elle est donc toute indiquée pour lire une texture : lire une texture n'est qu'un cas particulier de lecture de données. Les texels à lire sont à une adresse précise, la RAM répond à la lecture avec le texel demandé. Il est donc possible d'utiliser l'unité de communication avec la mémoire comme si c'était une unité de texture. Cependant, les textures ne sont pas utilisées comme telles de nos jours. Le rendu 3D moderne utilise des techniques dites de filtrage de texture, qui permettent d'améliorer la qualité du rendu des textures. Sans ce filtrage de texture, les textures appliquées naïvement donnent un résultat assez pixelisé et assez moche, pour des raisons assez techniques. Le filtrage élimine ces artefacts, en utilisant une forme d'''antialiasing'' interne aux textures, le fameux filtrage de texture. Le filtrage de texture peut être réalisé en logiciel ou en matériel. Techniquement, il est possible de le faire dans un ''shader''. Le ''shader'' calcule les adresses des texels à lire, lit les texels, et effectue ensuite le filtrage avec des opérations de calcul. Mais ce n'est pas ce qui est fait, le filtrage de texture est toujours effectué directement en matériel. La raison est que le filtrage de texture est très simple à implémenter en hardware. Le filtrage bilinéaire ou trilinéaire demande juste des circuits d'interpolation et quelques registres, ce qui est trivial. Et la seconde raison est qu'il n'y a pas 36 façons de filtrer des textures : une carte graphique peut implémenter les algorithmes principaux existants en assez peu de circuits. Pour simplifier l'implémentation, les processeurs de ''shader'' modernes disposent d'une unité d'accès mémoire séparée de l'unité de texture. L'unité d'accès mémoire normale s'occupe des accès mémoire hors-textures, alors que l'unité mémoire s'occupe de lire les textures. L'unité de texture contient de quoi faire du filtrage de texture, mais aussi faire des calculs d'adresse spécialisées, intrinsèquement liés au format des textures, qu'on détaillera dans le chapitre sur les textures. En comparaison, les unités d'accès mémoire effectuent des calculs d'adresse plus basiques. Un dernier avantage est que l'unité de texture est reliée au cache de texture, alors que l'unité d'accès mémoire est relié au cache L1/L2. ===Les ROPs peuvent être implémentés dans le ''pixel shader''=== Les ROPs effectuent plusieurs opérations basiques, mais les deux plus importantes sont la gestion du tampon de profondeur et de la transparence. Par transparence, on veut parler du mélange ''alpha''. Pour la gestion du tampon de profondeur, on veut parler du ''z-test'', qui compare la profondeur de deux pixels/fragments. Il s'agit d'opérations simples, qu'un processeur de shader peut faire sans problèmes. Par exemple, le ''z-test'' demande de faire plusieurs étapes : * calculer l'adresse du pixel dans le tampon de profondeur ; * lire le pixel dans le tampon de profondeur ; * Faire la comparaison entre profondeurs ; * Si le résultat de la comparaison est okay : ** écrire la nouvelle valeur z dans le tampon de profondeur, et écrire le nouveau pixel dedans. Le mélange ''alpha'' demande lui de : * calculer l'adresse du pixel dans le ''framebuffer'' ; * lire le pixel dans le ''framebuffer'' ; * faire des additions et multiplications pour le mélange ''alpha'' : * écrire le nouveau pixel dans le ''framebuffer''. Pour résumer il faut pouvoir faire : calcul d'adresse, lecture, écriture, addition, multiplication et comparaisons. Et toutes ces opérations sont supportées nativement par les processeurs de shaders, ce sont des instructions communes. Il est donc possible d'émuler les ROPs dans les pixels shaders. En pratique, c'est assez rare, et il y a une bonne explication à cela. Émuler les ROPs dans un ''pixel shader'' est trivial, comme on vient de le voir. Sauf que cela ne marche que si le GPU fait le rendu un pixel à la fois. Le tampon de profondeur est conçu pour traiter un pixel à la fois, idem pour le mélange ''alpha''. Mais si on ne traite pas l'image pixel par pixel, alors les deux algorithmes dysfonctionnent. Donc, tout va bien s'il n'y a qu'un seul processeur de ''pixel shader'', et que celui-ci est conçu pour ne traiter qu'un pixel à la fois, qu'une seule instance de ''shader''. Mais cela ne marche pas sur les GPU modernes, qui ont non seulement près d'une centaine de processeurs de shaders. Pour donner un exemple, imaginons la situation illustrée ci-dessous. Supposons que l'on ait assez de processeurs de shaders pour traiter plusieurs triangles en même temps. Par malchance, les processeurs rendent en même temps deux triangles opaques qui se recouvrent à l'écran. Là où ils se recouvrent, les deux triangles vont générer deux fragments par pixel, et un seul sera le bon. Pas de chance, les deux fragments sont rendus en parallèle dans deux processeurs séparés. Les deux processeurs lisent la même donnée dans le tampon de profondeur et les deux fragments passent le ''z-test'', car ils n'ont aucun moyen de savoir la coordonnée z en cours de traitement dans l'autre processeur. Les deux processeurs vont alors écrire leur résultat en mémoire et c'est premier arrivé, premier servi. Le résultat n'est pas forcément celui attendu : le pixel le plus proche peut être écrit avant le plus lointain, ou inversement. [[File:Situation où faire le z-test dans les pixel shaders dysfonctionne.png|centre|vignette|upright=2|Situation où faire le z-test dans les pixel shaders dysfonctionne]] Pour obtenir un bon rendu, le GPU doit forcer le z-test à se faire fragment par fragment, pour chaque pixel. Il ne doit pas être possible d'avoir deux fragments qui modifient le même pixel, dans deux processeurs de shaders séparés. Et pour garantir cela, il n'y a pas beaucoup de solutions. Une solution possible serait de mémoriser, pour chaque processeur de shader, les coordonnées du pixel qu'il traite à l'écran. Quand le rastériseur génère un fragment, il vérifie s'il y a conflit avec les fragments en cours de traitement, et attend si c'est le cas. Ou du moins, s'il fait des tests en parallèles, il doit faire en sorte que le résultat soit corrigé d'une manière ou d'une autre. En utilisant des processeurs de shaders qui travaillent en parallèle, cette contrainte est parfois brisée et le rendu donne des résultats incorrects. Le tampon de profondeur n'est pas conçu pour être parallélisé, idem pour le mélange ''alpha''. Il faut donc une sorte de point de synchronisation dans le pipeline pour éviter tout problème. Et c'est à ça que servent les ROPs. Les cartes graphiques pour PC ont des ROPs séparés des processeurs de ''shaders''. Par contre, quelques cartes graphiques destinées les smartphones et autres appareils mobiles font exception. Sur celles-ci, les unités de textures et les ROPs sont intégrés dans les processeurs de ''shaders'', aux côtés de l'unité d'accès mémoire LOAD/STORE. Il s'agit de cartes graphiques en rendu à tuiles. {{NavChapitre | book=Les cartes graphiques | prev=Les cartes graphiques : architecture de base | prevText=Les cartes graphiques : architecture de base | next=Les processeurs de shaders | nextText=Les processeurs de shaders }} {{autocat}} jlnozg1rigamu2dowuqgxrjem0abztn 763509 763508 2026-04-11T20:56:55Z Mewtow 31375 /* Les ROPs peuvent être implémentés dans le pixel shader */ 763509 wikitext text/x-wiki Il est intéressant d'étudier le hardware des cartes graphiques en faisant un petit résumé de leur évolution dans le temps. En effet, leur hardware a fortement évolué dans le temps. Et il serait difficile à comprendre le hardware actuel sans parler du hardware d'antan. En effet, une carte graphique moderne est partiellement programmable. Certains circuits sont totalement programmables, d'autres non. Et pour comprendre pourquoi, il faut étudier comment ces circuits ont évolués. Le hardware des cartes graphiques a fortement évolué dans le temps, ce qui n'est pas une surprise. Les évolutions de la technologie, avec la miniaturisation des transistors et l'augmentation de leurs performances a permis aux cartes graphiques d'incorporer de plus en plus de circuits avec les années. Avant l'invention des cartes graphiques, toutes les étapes du pipeline graphique étaient réalisées par le processeur : il calculait l'image à afficher, et l’envoyait à une carte d'affichage 2D. Au fil du temps, de nombreux circuits furent ajoutés, afin de déporter un maximum de calculs vers la carte vidéo. Le rendu 3D moderne est basé sur le placage de texture inverse, avec des coordonnées de texture, une correction de perspective, etc. Mais les anciennes consoles et bornes d'arcade utilisaient le placage de texture direct. Et cela a impacté le hardware des consoles/PCs de l'époque. Avec le placage de texture direct, il était primordial de calculer la géométrie, mais la rasterisation était le fait de VDC améliorés. Aussi, les premières bornes d'arcade 3D et les consoles de 5ème génération disposaient processeurs pour calculer la géométrie et de circuits d'application de textures très particuliers. A l'inverse, les PC utilisaient un rendu inverse, totalement différent. Sur les PC, les premières cartes graphiques avaient un circuit de rastérisation et des unités de textures, mais pas de circuits géométriques. ==Les premières cartes graphiques== Dès les années 70-80, le rendu 3D était utilisé par de nombreuses entreprises industrielles : des applications de visualisation 3D étaient utilisées en architecture, des applications de conception assistée par ordinateur étaient déjà d'utilisation courante, sans compter les simulateurs de vol utilisés par l'armée et les instructeurs qui formaient les pilotes d'avion. Le rendu 3D était aussi étudié au niveau académique, la recherche en 3D était déjà florissante. Il existait même du matériel spécifiquement conçu pour le rendu graphique, mais celui-ci était spécifiquement dédié à des super-calculateurs ou des ''workstations'' (des sortes d'ancêtres des PC, très puissants pour l'époque, mais conçus uniquement pour les entreprises). ===Le début des années 80 : le rendu en fils de fer=== Le tout premier système de ce genre était le '''''Line Drawing System-1''''' de l'entreprise Evans & Sutherland, daté de 1969. Ce n'est ni plus ni moins que le toute premier circuit graphique séparé du processeur ayant existé. C'est en un sens la toute première carte graphique, le tout premier GPU. Il prenait la forme d'un périphérique qui se connectait à l'ordinateur d'un côté et était relié à l'écran de l'autre. Il était compatible avec un grand nombre d'ordinateurs et de processeurs existants. Il a été suivi par plusieurs successeurs, nommés ''Picture System 1, 2'' et le ''PS300 series''. [[File:Evans & Sutherland LDS-1 (1).jpg|vignette|Evans & Sutherland LDS-1 (1)]] Ils permettaient de faire du rendu en fil de fer, sans texture ni même sans polygones colorés. Un tel rendu était utile pour des applications assez limitées : architecture, dessin de molécules pour les entreprises pharmaceutique et certains centres de recherche, l'aérospatiale, etc. Ces cartes graphiques étaient utilisées de concert avec des écrans appelés '''écrans vectoriels''' (''vector display''). Pour simplifier, ils ressemblaient à des écrans CRT, sauf que le faisceau d'électron ne balayait pas l'écran ligne par ligne, mais traçait des lignes arbitraires à l'écran. On lui précisait deux points de coordonnées x1,y1 ; et x2,y2 ; puis l'écran tracait une ligne entre ces deux points. En général, la ligne tracée était maintenue pendant un long moment, entre plusieurs secondes et plusieurs minutes. L'intérieur du circuit était assez simple : un circuit de multiplication de matrice pour les calculs géométriques, un rastériser simplifié (le ''clipping diviser''), un circuit de tracé de lignes, et un processeur de contrôle pour commander les autres circuits. Le fait que ces trois circuits soient séparés permettait une implémentation en pipeline, où plusieurs portions de l'image pouvaient être calculées en même temps : pendant que l'une est dans l'unité géométrique, l'autre est dans le rastériseur et une troisième est en cours de tracé. [[File:Lds1blockdiagram05.svg|centre|vignette|upright=2|Architecture du LDS-1. Le processeur de contrôle n'est pas représenté.]] Le processeur de contrôle exécute un programme qui se charge de commander l'unité géométrique et les autres circuits. Le programme en question est fourni par le programmeur, le LDS-1 est donc totalement programmable. Il lit directement les données nécessaires pour le rendu dans la mémoire de l’ordinateur et le programme exécuté est lui aussi en mémoire principale. Il n'a pas de mémoire vidéo dédiée, il utilise la RAM de l'ordinateur principal. Le multiplieur de matrices est plus complexe qu'on pourrait s'y attendre. Il ne s'agit pas que d'un circuit arithmétique tout simple, mais d'un véritable processeur avec des registres et des instructions machine complexes. Il contient plusieurs registres, l'ensemble mémorisant 4 matrices de 16 nombres chacune (4 lignes de 4 colonnes). Un nombre est codé sur 18 bits. Les registres sont reliés à un ensemble de circuits arithmétiques, des additionneurs et des multiplieurs. Le circuit supporte des instructions de copie entre registres, pour copier une ligne d'une matrice à une autre, des instructions LOAD/STORE pour lire ou écrire dans la mémoire RAM, etc. Il supporte aussi des multiplications en 2D et 3D. Le ''clipping divider'' est un circuit assez complexe, contenant un processeur à accumulateur, une mémoire ROM pour le programme du processeur. Le programme exécuté par le processeur est un petit programme de 62 instructions, stocké dans la ROM. L'algorithme du ''clipping divider'' est décrite dans le papier de recherche "A clipping divider", écrit par Robert Sproull. Un détail assez intéressant est que le résultat en sortie de l'unité géométrique et du rastériseur peuvent être envoyés à l'ordinateur en parallèle du rendu. C'était très utile sur les anciens ordinateurs qui étaient connectés à plusieurs terminaux. Le LDS-1 calculait la géométrie et le rendu, et le tout pouvait petre envoyé à d'autres composants, comme des terminaux, une imprimante, etc. ===Les systèmes ultérieurs : rendu à triangles colorés et texturé=== Les systèmes précédents étaient très limités : ils calculaient la géométrie et n'avaient pas de ''framebuffer'', ni de tampon de profondeur, ni gestion de l'éclairage, ni quoique ce soit. De tels systèmes étaient donc des accélérateurs géométriques que de vrais systèmes graphiques complets, du fait de l'absence de ''framebuffer''. Ils étaient composés de processeurs spécialisés dans les calculs à virgule flottante, faisant des calculs géométriques, et éventuellement d'un processeur pour la rastérisation. La raison est que la RAM était très chère et que créer des circuits fixes étaient très chers et peu disponibles. Par contre, les processeurs à virgule flottante étaient peu chers et facile à trouver. Vers la fin des années 80, grâce à la baisse du prix de la RAM et la démocratisation des ASIC (des circuits fixes fait sur mesure), ajouter un ''framebuffer'' est est devenu possible. C'est alors que sont apparus les '''systèmes de rendu 3D de première génération'''. De tels systèmes ont permis d'implémenter le rendu à primitives colorées qu'on a vu il y a quelques chapitres, à savoir un rendu où les triangles sont coloriés avec une couleur unique. Les systèmes de première génération étaient simples : des processeurs pour le calcul de la géométrie, un circuit de rastérisation, une RAM pour le ''framebuffer'' et des ASIC servant de ROPs très simples. Il n'y avait pas d'élimination des pixels cachés, pas de textures, et encore moins d'éclairage par pixels. Le premier système de ce genre était le ''Shaded Picture System'', toujours par Evans & Sutherland. Il ne gérait pas la couleur et ne pouvait afficher que des images en noir et blanc, mais il gérait l'éclairage par sommet (''vertex lighting''). Il a rapidement été dépassé par les systèmes de l'entreprise ''Silicon Graphics Inc'' (SGI), ainsi que ceux de l'entreprise Apollo avec sa série Apollo DN. Les '''systèmes de seconde génération''' sont apparus vers la fin des années 80, et se distinguent des précédents par l'ajout un tampon de profondeur. Ils intègrent aussi des capacités d'éclairage par pixel, à savoir de l'éclairage plat, de Gouraud, voire de Phong ! Enfin, les '''systèmes de troisième génération''' ont acquis des capacités de placage de texture, que les systèmes précédents n'avaient pas. Ils ont aussi ajouté un support de l'antialiasing. Les systèmes SGI avec placage de texture ont déjà été abordé au chapitre précédent, dans la section sur les GPU en mode immédiat et à ''tile''. Aussi, nous ne reviendrons pas dessus. [[File:Evolution de l'architecture des premières cartes graphiques, dans les années 80-90.png|centre|vignette|upright=2.5|Evolution de l'architecture des premières cartes graphiques, dans les années 80-90]] Les systèmes de première, seconde et troisième génération avaient de nombreux points communs. En premier lieu, ils étaient fabriqués en connectant plusieurs cartes électroniques : une carte pour les calculs géométriques, une ou plusieurs cartes pour le reste du rendu graphique, une carte dédiée au VDC et avec un connecteur écran. Les transistors de l'époque n'étaient pas encore miniaturisés, ce qui fait que le système graphique ne pouvait pas tenir sur une seule carte électronique. Il n'y avait donc pas de carte graphique proprement dit, mais un équivalent éclaté sur plusieurs cartes électroniques. La carte pour la géométrie contenait typiquement une mémoire FIFO pour accumuler les commandes de rendu, un processeur de commande, et plusieurs processeurs géométriques. Les processeurs géométriques étaient parfois conçus sur mesure, comme l'a été le le ''Geometry Engine'' de SGI. Mais il est arrivé qu'ils utilisent des processeurs commerciaux comme le Weitek 3222, l'Intel i860, etc. Les processeurs pouvaient être placés en série ou en parallèle, comme expliqué dans le chapitre précédent. Le circuit de rastérisation était réalisé soit avec un processeur dédié, soit avec un circuit fixe, soit un mélange des deux. La rastérisation est en effet réalisée en plusieurs étapes, certaines peuvent être implémentées avec un processeur et d'autres avec des circuits fixes. Un point important est qu'à l'époque, le rendu n'utilisait pas que des triangles, mais des polygones en général. Ce n'est que par la suite que le rendu s'est focalisé sur les triangles et les ''quads'' (quadrilatères). Il arrivait que le système graphique gérait partiellement des polygones concaves, voire convexes. Sur les systèmes SGI, les calculs géométriques se faisaient avec des polygones, que la rastérisation découpait en triangles, le reste du rendu se faisait avec des triangles. Les stations de travail Apollo DN 10000VS découpaient les polygones en trapézoïdes orientés à l'horizontale, alignés avec des ''scanlines''. D'autres systèmes découpaient tout en triangle lors de l'étape géométrique ==Les précurseurs grand public : les bornes d'arcade== [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] L'accélération du rendu 3D sur les bornes d'arcade était déjà bien avancé dès les années 90. Les bornes d'arcade ont toujours été un segment haut de gamme de l'industrie du jeu vidéo, aussi ce n'est pas étonnant. Le prix d'une borne d'arcade dépassait facilement les 10 000 dollars pour les plus chères et une bonne partie du prix était celui du matériel informatique. Le matériel était donc très puissant et débordait de mémoire RAM comparé aux consoles de jeu et aux PC. La plupart des bornes d'arcade utilisaient du matériel standardisé entre plusieurs bornes. A l'intérieur d'une borne d'arcade se trouve une '''carte de borne d'arcade''' qui est une carte mère avec un ou plusieurs processeurs, de la RAM, une carte graphique, un VDC et pas mal d'autres matériels. La carte est reliée aux périphériques de la borne : joysticks, écran, pédales, le dispositif pour insérer les pièces afin de payer, le système sonore, etc. Le jeu utilisé pour la borne est placé dans une cartouche qui est insérée dans un connecteur spécialisé. Les cartes de bornes d'arcade étaient généralement assez complexes, elles avaient une grande taille et avaient plus de composants que les cartes mères de PC. Chaque carte contenait un grand nombre de chips pour la mémoire RAM et ROM, et il n'était pas rare d'avoir plusieurs processeurs sur une même carte. Et il n'était pas rare d'avoir trois à quatre cartes superposées dans une seule borne. Pour ceux qui veulent en savoir plus, Fabien Sanglard a publié gratuitement un livre sur le fonctionnement des cartes d'arcade CPS System, disponible via ce lien : [https://fabiensanglard.net/b/cpsb.pdf The book of CP System]. Les premières cartes graphiques des bornes d'arcade étaient des cartes graphiques 2D auxquelles on avait ajouté quelques fonctionnalités. Les sprites pouvaient être tournés, agrandit/réduits, ou déformés pour simuler de la perspective et faire de la fausse 3D. Par la suite, le vrai rendu 3D est apparu sur les bornes d'arcade. Dès 1988, la carte d'arcade Namco System 21 et Sega Model 1 géraient les calculs géométriques. Quelques années plus tard, les cartes graphiques se sont mises à supporter un éclairage de Gouraud et du placage de texture. Par exemple, le Namco System 22 et la Sega model 2 supportaient des textures 2D et comme le filtrage de texture (bilinéaire et trilinéaire), le mip-mapping, et quelques autres. Au passage, les cartes graphiques de la Namco System 22 étaient développées en partenariat avec Eans & Sutherland, qui avait commencé à se diversifier dans le marché grand public. Les cartes graphiques de l'époque faisaient les calculs géométriques sur plusieurs processeurs, généralement des processeurs de type DSP (des processeurs spécialisés dans le traitement de signal). Par exemple, la Namco System 2 utilisait 4 DSP de marque Texas Instruments TMS320C25, cadencés à 24,576 MHz. La carte d'arcade Sega Model 1 utilisait quant à elle un DSP spécialisé dans les calculs géométriques. Par la suite, les bornes d'arcade ont réutilisé le hardware des PC et autres consoles de jeux. ==La 3D sur les consoles de quatrième/cinquième génération== Les consoles avant la quatrième génération de console étaient des consoles purement 2D, sans circuits d'accélération 3D. Leur carte graphique était un simple VDC 2D, plus ou moins performant selon la console. Les premières consoles de jeu capables de rendu 3D par elles-mêmes sont les consoles dites de 5ème génération. Il y a diverses manières de classer les consoles en générations, la plus commune place la 3D à la 5ème génération, mais détailler ces controverses quant à ce classement nous amènerait trop loin. Les consoles de génération avaient une architecture assez différente des systèmes antérieurs. Les systèmes SGI et assimilés pouvaient se permettre de couter assez cher, d'utiliser beaucoup de circuits, de prendre beaucoup de place. Les bornes d'arcade sont aussi dans ce cas. Aussi, il n'était pas rare que les cartes 3D de l'époque tiennent sur plusieurs cartes électroniques séparées. Mais une console ne peut pas se permettre ce genre de folies. Aussi, les cartes 3D des consoles de l'époque tenaient dans un seul circuit intégré, comme il est d'usage de nos jours. La conséquence est que certains circuits étaient fortement simplifiés, sur les consoles de cinquième génération. Et cela a impacté l'architecture interne des GPU des consoles. Les systèmes SGI avaient plusieurs processeurs pour calculer la géométrie, couplés à plusieurs unités non-programmables pour les pixels/textures. Les cartes 3D des consoles gardaient cette organisation : processeurs pour la géométrie, circuits fixes pour le reste. Mais elles se débrouillaient souvent avec un seul processeur, voire aucun ! Dans ce dernier cas, la géométrie était calculée sur le processeur principal, le CPU. Les unités pour les pixels étaient aussi moins nombreuses, mais il y en avait plusieurs, pour profiter de l'amplification des pixels. : Les cartes 3D des consoles de jeu utilisaient le placage de texture inverse, avec quelques exceptions qui utilisaient le placage de texture direct. ===Le rendu 3D sur les consoles de quatrième génération : la SNES=== Plus haut, j'ai dit que les consoles de quatrième génération n'avaient pas de carte accélératrice 3D. Pourtant, elles ont connus quelques jeux en vraie 3D. La raison à cela est que la 3D était calculée par un GPU placé dans les cartouches du jeu ! Par exemple, les cartouches de Starfox et de Super Mario 2 contenaient un coprocesseur Super FX, qui gérait des calculs de rendu 2D/3D. En tout, il y a environ 16 coprocesseurs pour la SNES et on en trouve facilement la liste sur le net. La console était conçue pour, des pins sur les ports cartouches étaient prévues pour des fonctionnalités de cartouche annexes, dont ces coprocesseurs. Ces pins connectaient le coprocesseur au bus des entrées-sorties. Les coprocesseurs des cartouches de NES avaient souvent de la mémoire rien que pour eux, qui était intégrée dans la cartouche. Ceci étant dit, passons aux consoles de cinquième génération. ===La Nintendo 64 : un GPU avancé=== La Nintendo 64 avait le GPU le plus complexe comparé aux autres consoles, et dépassait même les cartes graphiques des PC. Il faut dire que son GPU a été conçu avec l'aide de l'entreprise SGI, dont on a vu les systèmes graphiques plus haut. Le GPU de la N64 incorporait une unité pour les calculs géométriques, un circuit de rasterisation, une unité de textures et un ROP final pour les calculs de transparence/brouillard/antialiasing, ainsi qu'un circuit pour gérer la profondeur des pixels. En somme, tout le pipeline graphique était implémenté dans le GPU de la Nintendo 64, chose très en avance sur son temps, comparé au PC ou aux autres consoles ! Le GPU est construit autour d'un processeur dédié aux calculs géométriques, le ''Reality Signal Processor'' (RSP), autour duquel on a ajouté des circuits pour le reste du pipeline graphique. L'unité de calcul géométrique est un processeur MIPS R4000, un processeur assez courant à l'époque, auquel on avait retiré quelques fonctionnalités inutiles pour le rendu 3D. Il était couplé à 4 KB de mémoire vidéo, ainsi qu'à 4 KB de mémoire ROM. Le reste du GPU était réalisé avec des circuits fixes. Un point intéressant est que le programme exécuté par le RSP pouvait être programmé ! Le RSP gérait déjà des espèces de proto-shaders, qui étaient appelés des ''[https://ultra64.ca/files/documentation/online-manuals/functions_reference_manual_2.0i/ucode/microcode.html micro-codes]'' dans la documentation de l'époque. La ROM associée au RSP mémorise cinq à sept programmes différents, aux fonctionnalités différentes. * Les microcodes gspFast3D et gspF3DNoN, implémentent un rendu 3D normal, avec des options de ''clipping'' différentes entre les deux. * Le microcode gspTurbo3D fait la même chose, mais avec moins de fonctionnalités et avec une précision réduite. Il ne gère pas le ''clipping'', l'éclairage par pixel, la correction de perspective, l'antialiasing et quelques autres fonctionnalités. Il gère cependant l'éclairage de Gouraud. Il utilise une ''display list'' simplifiée comparé aux deux microcodes précédents. * Le microcode gspZ-Sort effectue une pré-passe z, à savoir qu'il calcule le tampon de profondeur final de la scène 3D, sans rendre l'image. Cela sert à faire une élimination des pixels cachés parfaite, en logiciel. On calcule le tampon de profondeur pour déterminer quels pixels sont visibles, puis une seconde passe rend l'image en, rejetant les pixels non-visibles. * Le microcode gspSprite2D implémente un rendu 2D émulé : les sprites et arrière-plan sont des rectangles texturés. Le microcode gspS2DEX fait la même chose, mais sert à émuler le rendu de la SNES plus qu'autre chose. * Le microcode gspLine3D ne gére que des lignes, pas de triangles. Il sert pour du rendu en fil de fer. Ils géraient le rendu 3D de manière différente et avec une gestion des ressources différentes. Très peu de studios de jeu vidéo ont développé leur propre microcodes N64, car la documentation était mal faite, que Nintendo ne fournissait pas de support officiel pour cela, que les outils de développement ne permettaient pas de faire cela proprement et efficacement. ===La Playstation 1=== Sur la Playstation 1 le calcul de la géométrie était réalisé par le processeur, la carte graphique gérait tout le reste. Et la carte graphique était un circuit fixe spécialisé dans la rasterisation et le placage de textures. Elle utilisait, comme la Nintendo 64, le placage de texture inverse, qui est apparu ensuite sur les cartes graphiques. ===La 3DO et la Sega Saturn=== La Sega Saturn et la 3DO étaient les deux seules consoles à utiliser le rendu direct. La géométrie était calculée sur le processeur, même si les consoles utilisaient parfois un CPU dédié au calcul de la géométrie. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. La Sega Saturn incorpore trois processeurs et deux GPU. Les deux GPUs sont nommés le VDP1 et le VDP2. Le VDP1 s'occupe des textures et des sprites, le VDP2 s'occupe uniquement de l'arrière-plan et incorpore un VDC tout ce qu'il y a de plus simple. Ils ne gèrent pas du tout la géométrie, qui est calculée par les trois processeurs. Le troisième processeur, la Saturn Control Unit, est un processeur de type DSP, à savoir un processeur spécialisé dans le traitement de signal. Il est utilisé presque exclusivement pour accélérer les calculs géométriques. Il avait sa propre mémoire RAM dédiée, 32 KB de SRAM, soit une mémoire locale très rapide. Les transferts entre cette RAM et le reste de l'ordinateur était géré par un contrôleur DMA intégré dans le DSP. En somme, il s'agit d'une sorte de processeur spécialisé dans la géométrie, une sorte d'unité géométrique programmable. Mais la géométrie n'était pas forcément calculée que sur ce DSP, mais pouvait être prise en charge par les 3 CPU. ==L'historique des cartes graphiques des PC, avant l'arrivée de Direct X et Open Gl== Sur PC, l'évolution des cartes graphiques a eu du retard par rapport aux consoles. Les PC sont en effet des machines multi-usage, pour lesquelles le jeu vidéo était un cas d'utilisation parmi tant d'autres. Et les consoles étaient la plateforme principale pour jouer à des jeux vidéo, le jeu vidéo PC étant plus marginal. Mais cela ne veut pas dire que le jeu PC n'existait pas, loin de là ! Un problème pour les jeux PC était que l'écosystème des PC était aussi fragmenté en plusieurs machines différentes : machines Apple 1 et 2, ordinateurs Commdore et Amiga, IBM PC et dérivés, etc. Aussi, programmer des jeux PC n'était pas mince affaire, car les problèmes de compatibilité étaient légion. C'est seulement quand la plateforme x86 des IBM PC s'est démocratisée que l'informatique grand public s'est standardisée, réduisant fortement les problèmes de compatibilité. Mais cela n'a pas suffit, il a aussi fallu que les API 3D naissent. Les API 3D comme Direct X et Open GL sont absolument cruciales pour garantir la compatibilité entre plusieurs ordinateurs aux cartes graphiques différentes. Aussi, l'évolution des cartes graphiques pour PC s'est faite main dans la main avec l'évolution des API 3D. Les fonctionnalités des cartes graphiques ont évolué dans le temps, en suivant les évolutions des API 3D. Du moins dans les grandes lignes, car il est arrivé plusieurs fois que des fonctionnalités naissent sur les cartes graphiques, pour que les fabricants forcent la main de Microsoft ou d'Open GL pour les intégrer de force dans les API 3D. Passons. ===L'introduction des premiers jeux 3D : Quake et les drivers miniGL=== L'API OpenGL est née de la main de SGI, encore eux ! SGI avait créé l'API Iris GL pour ses stations de travail Iris Graphics. Iris GL a ensuite été libéré et est devenu le standard Open GL. Open GL existait déjà avant l'apparition des cartes accélératrices 3D. Il y a avait donc déjà un terreau que les programmeurs graphiques pouvaient utiliser. Mais Open GL était surtout utilisé pour des applications industrielles, médicales (imagerie), graphiques ou militaires, pas pour le jeu vidéo. Mais cela changea avec la sortie du jeu Quake, d'IdSoftware, en 1996. Quake pouvait fonctionner en rendu logiciel, mais le programmeur responsable du moteur 3D (le célébre John Carmack) ajouta une version OpenGL du jeu. Il faut dire que le jeu était programmé sur une station de travail compatible avec OpenGL, même si aucune carte accélératrice de l'époque ne supportait OpenGL. C'était là un choix qui se révéla visionnaire. En théorie, le rendu par OpenGL aurait dû se faire intégralement en logiciel, sauf sur quelques rares stations de travail adaptées. Mais les premières cartes graphiques étaient déjà dans les starting blocks. La toute première carte 3D pour PC est la '''Rendition Vérité V1000''', sortie en Septembre 1995, soit quelques mois avant l'arrivée de la Nintendo 64. La Rendition Vérité V1000 contenait un processeur MIPS cadencé à 25 MHz, 4 mébioctets de RAM, une ROM pour le BIOS, et un RAMDAC, rien de plus. C'était un vrai ordinateur complètement programmable de bout en bout, sans aucun circuit fixe. Les programmeurs ne pouvaient cependant pas utiliser cette programmabilité avec des ''shaders'', mais elle permettait à Rendition d'implémenter n'importe quelle API 3D, que ce soit OpenGL, DirectX ou même sa son API propriétaire. La Rendition Vérité avait de bonnes performances pour ce qui est de la géométrie, mais pas pour le reste. Réaliser la rastérisation et le placage de texture en logiciel n'est pas efficace, pareil pour les opérations de fin de pipeline comme l'antialiasing. Le manque d'unités fixes très rapides pour la rastérisation, le placage de texture ou les opérations de fin de pipeline était clairement un gros défaut. Mais la Rendition Vérité était un cas à part, une exception dans le paysage des cartes 3D de l'époque, qui ne faisait rien comme les autres. Les autres cartes graphiques, sorties peu après, étaient les Voodoo de 3dfx, les Riva TNT de NVIDIA, les Rage/3D d'ATI, la Virge/3D de S3, et la Matrox Mystique. Elles avaient choisit le compromis inverse de la Rendition Vérité V1000 : de bonnes performances pour le placage de textures et la rastérization, mais pas pour les calculs géométriques. Pour rappel, les systèmes professionnels et les consoles avaient des processeurs pour la géométrie, et des circuits fixes pour le reste. Les cartes graphiques de PC se passaient des processeurs pour la géométrie, les calculs géométriques étaient réalisés par le CPU. Les toutes premières cartes 3D pour PC contenaient seulement des circuits pour gérer les textures et des ROPs. Elle géraient le ''z-buffer'' en mémoire vidéo, ainsi que des effets de brouillard. Il n'y avait même pas de circuit pour la rastérisation, qui était faite en logiciel, avec les calculs géométriques. [[File:Architecture de base d'une carte 3D - 2.png|centre|vignette|upright=1.5|Carte 3D sans rasterization matérielle.]] Les cartes suivantes ajoutèrent une gestion des étapes de ''rasterization'' directement en matériel. Les cartes ATI rage 2, les Invention de chez Rendition, et d'autres cartes graphiques supportaient la rasterisation en hardware. [[File:Architecture de base d'une carte 3D - 3.png|centre|vignette|upright=1.5|Carte 3D avec gestion de la géométrie.]] Pour exploiter les unités de texture et le circuit de rastérisation, OpenGL et Direct 3D étaient partiellement implémentées en logiciel, car les cartes graphiques ne supportaient pas toutes les fonctionnalités de l'API. C'était l'époque du miniGL, des implémentations partielles d'OpenGL, fournies par les fabricants de cartes 3D, implémentées dans les pilotes de périphériques de ces dernières. Les fonctionnalités d'OpenGL implémentées dans ces pilotes étaient presque toutes exécutées en matériel, par la carte graphique. Avec l'évolution du matériel, les pilotes de périphériques devinrent de plus en plus complets, au point de devenir des implémentations totales d'OpenGL. Mais au-delà d'OpenGL, chaque fabricant de carte graphique avait sa propre API propriétaire, qui était gérée par leurs pilotes de périphériques (''drivers''). Par exemple, les premières cartes graphiques de 3dfx interactive, les fameuses voodoo, disposaient de leur propre API graphique, l'API Glide. Elle facilitait la gestion de la géométrie et des textures, ce qui collait bien avec l'architecture de ces cartes 3D. Mais ces API propriétaires tombèrent rapidement en désuétude avec l'évolution de DirectX et d'OpenGL. Direct X était une API dans l'ombre d'Open GL. La première version de Direct X qui supportait la 3D était DirectX 2.0 (juin 2, 1996), suivie rapidement par DirectX 3.0 (septembre 1996). Elles dataient d'avant le jeu Quake, et elles étaient très éloignées du hardware des premières cartes graphiques. Elles utilisaient un système d'''execute buffer'' pour communiquer avec la carte graphique, Microsoft espérait que le matériel 3D implémenterait ce genre de système. Ce qui ne fu pas le cas. Direct X 4.0 a été abandonné en cours de développement pour laisser à une version 5.0 assez semblable à la 2.0/3.0. Le mode de rendu laissait de côté les ''execute buffer'' pour coller un peu plus au hardware de l'époque. Mais rien de vraiment probant comparé à Open GL. Même Windows utilisait Open GL au lieu de Direct X maison... C'est avec Direct X 6.0 que Direct X est entré dans la cours des grands. Il gérait la plupart des technologies supportées par les cartes graphiques de l'époque. ===Le ''multi-texturing'' de l'époque Direct X 6.0 : combiner plusieurs textures=== Une technologie très importante standardisée par Dirext X 6 est la technique du '''''multi-texturing'''''. Avec ce qu'on a dit dans le chapitre précédent, vous pensez sans doute qu'il n'y a qu'une seule texture par objet, qui est plaquée sur sa surface. Mais divers effet graphiques demandent d'ajouter des textures par dessus d'autres textures. En général, elles servent pour ajouter des détails, du relief, sur une surface pré-existante. Un exemple intéressant vient des jeux de tir : ajouter des impacts de balles sur les murs. Pour cela, on plaque une texture d'impact de balle sur le mur, à la position du tir. Il s'agit là d'un exemple de '''''decals''''', des petites textures ajoutées sur les murs ou le sol, afin de simuler de la poussière, des impacts de balle, des craquelures, des fissures, des trous, etc. Les textures en question sont de petite taille et se superposent à une texture existante, plus grande. Rendre des ''decals'' demande de pouvoir superposer deux textures. Direct X 6.0 supportait l'application de plusieurs textures directement dans le matériel. La carte graphique devait être capable d'accéder à deux textures en même temps, ou du moins faire semblant que. Pour cela, elle doublaient les unités de texture et adaptaient les connexions entre unités de texture et mémoire vidéo. La mémoire vidéo devait être capable de gérer plusieurs accès mémoire en même temps et devait alors avoir un débit binaire élevé. [[File:Multitexturing.png|centre|vignette|upright=2|Multitexturing]] La carte graphique devait aussi gérer de quoi combiner deux textures entre elles. Par exemple, pour revenir sur l'exemple d'une texture d'impact de balle, il faut que la texture d'impact recouvre totalement la texture du mur. Dans ce cas, la combinaison est simple : la première texture remplace l'ancienne, là où elle est appliquée. Mais les cartes graphiques ont ajouté d'autres combinaisons possibles, par exemple additionner les deux textures entre elle, faire une moyenne des texels, etc. Les opérations pour combiner les textures était le fait de circuits appelés des '''''combiners'''''. Concrètement, les ''combiners'' sont de simples unités de calcul. Les ''conbiners'' ont beaucoup évolués dans le temps, mais les premières implémentation se limitaient à quelques opérations simples : addition, multiplication, superposition, interpolation. L'opération effectuer était envoyée au ''conbiner'' sur une entrée dédiée. [[File:Multitexturing avec combiners.png|centre|vignette|upright=2|Multitexturing avec combiners]] S'il y avait eu un seul ''conbiner'', le circuit de ''multitexturing'' aurait été simplement configurable. Mais dans la réalité, les premières cartes utilisant du ''multi-texturing'' utilisaient plusieurs ''combiners'' placés les uns à la suite des autres. L'implémentation des ''combiners'' retenue par Open Gl, et par le hardware des cartes graphiques, était la suivante. Les ''combiners'' étaient placés en série, l'un à la suite de l'autre, chacun combinant le résultat de l'étage précédent avec une texture. Le premier ''combiner'' gérait l'éclairage par sommet, afin de conserver un minimum de rétrocompatibilité. [[File:Texture combiners Open GL.png|centre|vignette|upright=2|Texture combiners Open GL]] Voici les opérations supportées par les ''combiners'' d'Open GL. Ils prennent en entrée le résultat de l'étage précédent et le combinent avec une texture lue depuis l'unité de texture. {|class="wikitable" |+ Opérations supportées par les ''combiners'' d'Open GL |- ! Replace | colspan="2" | Pixel provenant de l'unité de texture |- ! Addition | colspan="2" | Additionne l'entrée au texel lu. |- ! Modulate | colspan="2" | Multiplie l'entrée avec le texel lu |- ! Mélange (''blending'') | Moyenne pondérée des deux entrées, pondérée par la composante de transparence || La couleur de transparence du texel lu et de l'entrée sont multipliées. |- ! Decals | Moyenne pondérée des deux entrées, pondérée par la composante de transparence. || La transparence du résultat est celle de l'entrée. |} Il faut noter qu'un dernier étage de ''combiners'' s'occupait d'ajouter la couleur spéculaire et les effets de brouillards. Il était à part des autres et n'était pas configurable, c'était un étage fixe, qui était toujours présent, peu importe le nombre de textures utilisé. Il était parfois appelé le '''''combiner'' final''', terme que nous réutiliserons par la suite. Mine de rien, cela a rendu les cartes graphiques partiellement programmables. Le fait qu'il y ait des opérations enchainées à la suite, opérations qu'on peut choisir librement, suffit à créer une sorte de mini-programme qui décide comment mélanger plusieurs textures. Mais il y avait une limitation de taille : le fait que les données soient transmises d'un étage à l'autre, sans détours possibles. Par exemple, le troisième étage ne pouvait avoir comme seule opérande le résultat du second étage, mais ne pouvait pas utiliser celui du premier étage. Il n'y avait pas de registres pour stocker ce qui sortait de la rastérisation, ni pour mémoriser temporairement les texels lus. ===Le ''Transform & Lighting'' matériel de Direct X 7.0=== [[File:Architecture de base d'une carte 3D - 4.png|vignette|upright=1.5|Carte 3D avec gestion de la géométrie.]] La première carte graphique pour PC capable de gérer la géométrie en hardware fût la Geforce 256, la toute première Geforce. Son unité de gestion de la géométrie n'est autre que la bien connue '''unité T&L''' (''Transform And Lighting''). Elle implémentait des algorithmes d'éclairage de la scène 3D assez simples, comme un éclairage de Gouraud, qui étaient directement câblés dans ses circuits. Mais contrairement à la Nintendo 64 et aux bornes d'arcade, elle implémentait le tout, non pas avec un processeur classique, mais avec des circuits fixes. Avec Direct X 7.0 et Open GL 1.0, l'éclairage était en théorie limité à de l'éclairage par sommet, l'éclairage par pixel n'était pas implémentable en hardware. Les cartes graphiques ont tenté d'implémenter l'éclairage par pixel, mais cela n'est pas allé au-delà du support de quelques techniques de ''bump-mapping'' très limitées. Par exemple, Direct X 6.0 implémentait une forme limitée de ''bump-mapping'', guère plus. Un autre problème est qu'il a beaucoup d'algorithmes d'éclairages différents, aux résultats visuels différents, bien au-delà des algorithmes d'éclairage plat, de Gouraud et de Phong. Et les unités de T&L étaient souvent en retard sur les algorithmes logiciels. Les programmeurs avaient le choix entre programmer les algorithmes d’éclairage qu'ils voulaient et les exécuter en logiciel, ou utiliser ceux de l'unité de T&L. Ils choisissaient souvent la première option. Par exemple, Quake 3 Arena et Unreal Tournament n'utilisaient pas les capacités d'éclairage géométrique et préféraient utiliser leurs calculs d'éclairage logiciel fait maison. Cependant, le hardware dépassait les capacités des API et avait déjà commencé à ajouter des capacités de programmation liées au ''multi-texturing''. Les cartes graphiques de l'époque, surtout chez NVIDIA, implémentaient un système de '''''register combiners''''', une forme améliorée de ''texture combiners'', qui permettait de faire une forme limitée d'éclairage par pixel, notamment du vrai ''bump-mampping'', voire du ''normal-mapping''. Mais ce n'était pas totalement supporté par les API 3D de l'époque. Les ''registers combiners'' sont des ''texture combiners'' mais dans lesquels ont aurait retiré la stricte organisation en série. Il y a toujours plusieurs étages à la suite, qui peuvent exécuter chacun une opération, mais tous les étages ont maintenant accès à toutes les textures lues et à tout ce qui sort de la rastérisation, pas seulement au résultat de l'étape précédente. Pour cela, on ajoute des registres pour mémoriser ce qui sort des unités de texture, et pour ce qui sort de la rastérisation. De plus, on ajoute des registres temporaires pour mémoriser les résultats de chaque ''combiner'', de chaque étage. Il faut cependant signaler qu'il existe un ''combiner'' final, séparé des étages qui effectuent des opérations proprement dits. Il s'agit de l'étage qui applique la couleur spéculaire et les effets de brouillards. Il ne peut être utilisé qu'à la toute fin du traitement, en tant que dernier étage, on ne peut pas mettre d'opérations après lui. Sa sortie est directement connectée aux ROPs, pas à des registres. Il faut donc faire la distinction entre les '''''combiners'' généraux''' qui effectuent une opération et mémorisent le résultat dans des registres, et le ''combiner'' final qui envoie le résultat aux ROPs. L'implémentation des ''register combiners'' utilisait un processeur spécialisés dans les traitements sur des pixels, une sorte de proto-processeur de ''shader''. Le processeur supportait des opérations assez complexes : multiplication, produit scalaire, additions. Il s'agissait d'un processeur de type VLIW, qui sera décrit dans quelques chapitres. Mais ce processeur avait des programmes très courts. Les premières cartes NVIDIA, comme les cartes TNT pouvaient exécuter deux opérations à la suite, suivie par l'application de la couleurs spéculaire et du brouillard. En somme, elles étaient limitées à un ''shader'' à deux/trois opérations, mais c'était un début. Le nombre d'opérations consécutives est rapidement passé à 8 sur la Geforce 3. ===L'arrivée des ''shaders'' avec Direct X 8.0=== [[File:Architecture de la Geforce 3.png|vignette|upright=1.5|Architecture de la Geforce 3]] Les ''register combiners'' était un premier pas vers un éclairage programmable. Paradoxalement, l'évolution suivante s'est faite non pas dans l'unité de rastérisation/texture, mais dans l'unité de traitement de la géométrie. La Geforce 3 a remplacé l'unité de T&L par un processeur capable d'exécuter des programmes. Les programmes en question complétaient l'unité de T&L, afin de pouvoir rajouter des techniques d'éclairage plus complexes. Le tout a permis aussi d'ajouter des animations, des effets de fourrures, des ombres par ''shadow volume'', des systèmes de particule évolués, et bien d'autres. À partir de la Geforce 3 de Nvidia, les cartes graphiques sont devenues capables d'exécuter des programmes appelés '''''shaders'''''. Le terme ''shader'' vient de ''shading'' : ombrage en anglais. Grace aux ''shaders'', l'éclairage est devenu programmable, il n'est plus géré par des unités d'éclairage fixes mais été laissé à la créativité des programmeurs. Les programmeurs ne sont plus vraiment limités par les algorithmes d'éclairage implémentés dans les cartes graphiques, mais peuvent implémenter les algorithmes d'éclairage qu'ils veulent et peuvent le faire exécuter directement sur la carte graphique. Les ''shaders'' sont classifiés suivant les données qu'ils manipulent : '''''pixel shader''''' pour ceux qui manipulent des pixels, '''''vertex shaders''''' pour ceux qui manipulent des sommets. Les premiers sont utilisés pour implémenter l'éclairage par pixel, les autres pour gérer tout ce qui a trait à la géométrie, pas seulement l'éclairage par sommets. Direct X 8.0 avait un standard pour les shaders, appelé ''shaders 1.0'', qui correspondait parfaitement à ce dont était capable la Geforce 3. Il standardisait les ''vertex shaders'' de la Geforce 3, mais il a aussi renommé les ''register combiners'' comme étant des ''pixel shaders'' version 1.0. Les ''register combiners'' n'ont pas évolués depuis la Geforce 256, si ce n'est que les programmes sont passés de deux opérations successives à 8, et qu'il y avait possibilité de lire 4 textures en ''multitexturing''. A l'opposé, le processeur de ''vertex shader'' de la Geforce 3 était capable d'exécuter des programmes de 128 opérations consécutives et avait 258 registres différents ! Des ''pixels shaders'' plus évolués sont arrivés avec l'ATI Radeon 8500 et ses dérivés. Elle incorporait la technologie ''SMARTSHADER'' qui remplacait les ''registers combiners'' par un processeur de ''shader'' un peu limité. Un point est que le processeur acceptait de calculer des adresses de texture dans le ''pixel shader''. Avant, les adresses des texels à lire étaient fournis par l'unité de rastérisation et basta. L'avantage est que certains effets graphiques étaient devenus possibles : du ''bump-mapping'' avancé, des textures procédurales, de l'éclairage par pixel anisotrope, du éclairage de Phong réel, etc. Avec la Radeon 8500, le ''pixel shader'' pouvait calculer des adresses, et lire les texels associés à ces adresses calculées. Les ''pixel shaders'' pouvaient lire 6 textures, faire 8 opérations sur les texels lus, puis lire 6 textures avec les adresses calculées à l'étape précédente, et refaire 8 opérations. Quelque chose de limité, donc, mais déjà plus pratique. Les ''pixel shaders'' de ce type ont été standardisé dans Direct X 8.1, sous le nom de ''pixel shaders 1.4''. Encore une fois, le hardware a forcé l'intégration dans une API 3D. ===Les ''shaders'' de Direct X 9.0 : de vrais ''pixel shaders''=== Avec Direct X 9.0, les ''shaders'' sont devenus de vrais programmes, sans les limitations des ''shaders'' précédents. Les ''pixels shaders'' sont passés à la version 2.0, idem pour les ''vertex shaders''. Concrètement, ils ont des fonctionnalités bien supérieures à celles des ''registers combiners''. Les ''shaders'' pouvaient exécuter une suite d'opérations arbitraire, dans le sens où elle n'était pas structurée avec tel type d'opération au début, suivie par un accès aux textures, etc. On pouvait mettre n'importe quelle opération dans n'importe quel ordre. De plus, les ''shaders'' ne sont plus écrit en assembleur comme c'était le cas avant. Ils sont dorénavant écrits dans un langage de haut-niveau, le HLSL pour les shaders Direct X et le GLSL pour les shaders Open Gl. Les ''shaders'' sont ensuite traduit (compilés) en instructions machines compréhensibles par la carte graphique. Au début, ces langages et la carte graphique supportaient uniquement des opérations simples. Mais au fil du temps, les spécifications de ces langages sont devenues de plus en plus riches à chaque version de Direct X ou d'Open Gl, et le matériel en a fait autant. Le matériel s'est alors adapté, en incorporant un véritable processeur pour les ''pixel shaders''. Les ''pixel shaders'' sont maintenant exécutés par un processeur de ''shader'' dédié, aux fonctionnalités bien supérieures à celles des ''registers combiners''. Le processeur de ''pixel shader'' incorpore l'unité de texture en sont sein, les deux sont fusionnés. La raison à cela sera expliqué dans la suite du chapitre. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=1.5|Carte 3D avec pixels et vertex shaders non-unifiés.]] ===L'après Direct X 9.0=== Avant Direct X 10, les processeurs de ''shaders'' ne géraient pas exactement les mêmes opérations pour les processeurs de ''vertex shader'' et de ''pixel shader''. Les processeurs de ''vertex shader'' et de ''pixel shader''étaient séparés. Depuis DirectX 10, ce n'est plus le cas : le jeu d'instructions a été unifié entre les vertex shaders et les pixels shaders, ce qui fait qu'il n'y a plus de distinction entre processeurs de vertex shaders et de pixels shaders, chaque processeur pouvant traiter indifféremment l'un ou l'autre. [[File:Architecture de base d'une carte 3D - 6.png|centre|vignette|upright=1.5|Architecture de la GeForce 6800.]] Avec Direct X 10, de nombreux autres ''shaders'' sont apparus. Les plus liés au rendu 3D sont les '''''geometry shader''''' pour ceux qui manipulent des triangles, de ''hull shaders'' et de ''domain shaders'' pour la tesselation. De plus, les cartes graphiques modernes sont capables d’exécuter des programmes informatiques qui n'ont aucun lien avec le rendu 3D, mais sont exécutés par la carte graphique comme le ferait un processeur d'ordinateur normal. De tels ''shaders'' sans lien avec le rendu 3D sont appelés des ''compute shader''. ==Les cartes graphiques d'aujourd'hui== Les circuits d'un GPU ont beaucoup évolué depuis l'introduction des ''shaders'', pour devenir de plus en plus programmables. Mais à côté des processeurs de ''shaders'', il reste quelques circuits non-programmables appelés des circuits fixes. La rastérisation, le placage de texture, l'élimination des pixels cachés et le mélange ''alpha'' sont gérés par des circuits fixes. [[File:3D-Pipeline.svg|centre|vignette|upright=3.0|Pipeline 3D : ce qui est programmable et ce qui ne l'est pas dans une carte graphique moderne.]] Mais pourquoi ne pas tout rendre programmable ? Ou au contraire, utiliser seulement des circuits fixes ? La réponse rapide est qu'il s'agit d'un compromis entre flexibilité et performance qui permet d'avoir le meilleur des deux mondes. Mais ce compromis a fortement évolué dans le temps, comme on va le voir plus bas. Rendre l'éclairage programmable permet d'implémenter facilement un grand nombre d'effets graphiques sans avoir à les implémenter en hardware. Avant les ''shaders'', les effets graphiques derniers cri n'étaient disponibles que sur les derniers modèles de carte graphique. Avec des ''vertex/pixel shaders'', ce genre de défaut est passé à la trappe. Si un nouvel algorithme de rendu graphique est inventé, il peut être utilisé dès le lendemain sur toutes les cartes graphiques modernes. De plus, implémenter beaucoup d'algorithmes d'éclairage différents avec des circuits fixes a un cout en termes de transistors, alors qu'utiliser des circuits programmable a un cout en hardware plus limité. Tout cela est à l'exact opposé de ce qu'on a avec les autres circuits, comme les circuits pour la rastérisation ou le placage de texture. Il n'y a pas 36 façons de rastériser une scène 3D et la flexibilité n'est pas un besoin important pour cette opération, alors que les performances sont cruciales. Même chose pour le placage/filtrage de textures. En conséquences, les unités de rastérisation, de texture, et les ROPs sont toutes implémentées en matériel. Faire ainsi permet de gagner en performance sans que cela ait le moindre impact pour le programmeur. Reste à expliquer dans le détail pourquoi. ===Les unités de texture sont intégrées aux processeurs de shaders=== Avec l'arrivée des processeurs de shaders, les unités de texture ont été intégrées dans les processeurs de shaders eux-mêmes. C'est la seule unité fixe qui a subit ce traitement, et il est intéressant de comprendre pourquoi. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=2|Architecture de base d'une carte 3D.]] Pour cela, il faut faire un rappel sur ce qu'il y a dans un processeur. Un processeur contient globalement quatre circuits : * une unité de calcul qui fait des calculs ; * des registres pour stocker les opérandes et résultats des calculs ; * une unité de communication avec la mémoire ; * et un séquenceur, un circuit de contrôle qui commande les autres. L'unité de communication avec la mémoire sert à lire ou écrire des données, à les transférer de la RAM vers les registres, ou l'inverse. Lire une donnée demande d'envoyer son adresse à la RAM, qui répond en envoyant la donnée lue. Elle est donc toute indiquée pour lire une texture : lire une texture n'est qu'un cas particulier de lecture de données. Les texels à lire sont à une adresse précise, la RAM répond à la lecture avec le texel demandé. Il est donc possible d'utiliser l'unité de communication avec la mémoire comme si c'était une unité de texture. Cependant, les textures ne sont pas utilisées comme telles de nos jours. Le rendu 3D moderne utilise des techniques dites de filtrage de texture, qui permettent d'améliorer la qualité du rendu des textures. Sans ce filtrage de texture, les textures appliquées naïvement donnent un résultat assez pixelisé et assez moche, pour des raisons assez techniques. Le filtrage élimine ces artefacts, en utilisant une forme d'''antialiasing'' interne aux textures, le fameux filtrage de texture. Le filtrage de texture peut être réalisé en logiciel ou en matériel. Techniquement, il est possible de le faire dans un ''shader''. Le ''shader'' calcule les adresses des texels à lire, lit les texels, et effectue ensuite le filtrage avec des opérations de calcul. Mais ce n'est pas ce qui est fait, le filtrage de texture est toujours effectué directement en matériel. La raison est que le filtrage de texture est très simple à implémenter en hardware. Le filtrage bilinéaire ou trilinéaire demande juste des circuits d'interpolation et quelques registres, ce qui est trivial. Et la seconde raison est qu'il n'y a pas 36 façons de filtrer des textures : une carte graphique peut implémenter les algorithmes principaux existants en assez peu de circuits. Pour simplifier l'implémentation, les processeurs de ''shader'' modernes disposent d'une unité d'accès mémoire séparée de l'unité de texture. L'unité d'accès mémoire normale s'occupe des accès mémoire hors-textures, alors que l'unité mémoire s'occupe de lire les textures. L'unité de texture contient de quoi faire du filtrage de texture, mais aussi faire des calculs d'adresse spécialisées, intrinsèquement liés au format des textures, qu'on détaillera dans le chapitre sur les textures. En comparaison, les unités d'accès mémoire effectuent des calculs d'adresse plus basiques. Un dernier avantage est que l'unité de texture est reliée au cache de texture, alors que l'unité d'accès mémoire est relié au cache L1/L2. ===Les ROPs peuvent être implémentés dans le ''pixel shader''=== Les ROPs effectuent plusieurs opérations basiques, mais les deux plus importantes sont la gestion du tampon de profondeur et de la transparence. Par transparence, on veut parler du mélange ''alpha''. Pour la gestion du tampon de profondeur, on veut parler du ''z-test'', qui compare la profondeur de deux pixels/fragments. Il s'agit d'opérations simples, qu'un processeur de shader peut faire sans problèmes. Par exemple, le ''z-test'' demande de faire plusieurs étapes : * calculer l'adresse du pixel dans le tampon de profondeur ; * lire le pixel dans le tampon de profondeur ; * Faire la comparaison entre profondeurs ; * Si le résultat de la comparaison est okay : ** écrire la nouvelle valeur z dans le tampon de profondeur, et écrire le nouveau pixel dedans. Le mélange ''alpha'' demande lui de : * calculer l'adresse du pixel dans le ''framebuffer'' ; * lire le pixel dans le ''framebuffer'' ; * faire des additions et multiplications pour le mélange ''alpha'' : * écrire le nouveau pixel dans le ''framebuffer''. Pour résumer il faut pouvoir faire : calcul d'adresse, lecture, écriture, addition, multiplication et comparaisons. Et toutes ces opérations sont supportées nativement par les processeurs de shaders, ce sont des instructions communes. Il est donc possible d'émuler les ROPs dans les pixels shaders. En pratique, c'est assez rare, et il y a une bonne explication à cela. Émuler les ROPs dans un ''pixel shader'' est trivial, comme on vient de le voir. Sauf que cela ne marche que si le GPU fait le rendu un pixel à la fois. Le tampon de profondeur est conçu pour traiter un pixel à la fois, idem pour le mélange ''alpha''. Mais si on ne traite pas l'image pixel par pixel, alors les deux algorithmes dysfonctionnent. Donc, tout va bien s'il n'y a qu'un seul processeur de ''pixel shader'', et que celui-ci est conçu pour ne traiter qu'un pixel à la fois, qu'une seule instance de ''shader''. Mais cela ne marche pas sur les GPU modernes, qui ont non seulement près d'une centaine de processeurs de shaders, chacun étant conçu pour traiter une centaine de fragments/pixels en même temps ! Pour donner un exemple, imaginons la situation illustrée ci-dessous. Supposons que l'on ait assez de processeurs de shaders pour traiter plusieurs triangles en même temps. Par malchance, les processeurs rendent en même temps deux triangles opaques qui se recouvrent à l'écran. Là où ils se recouvrent, les deux triangles vont générer deux fragments par pixel, et un seul sera le bon. Pas de chance, les deux fragments sont rendus en parallèle dans deux processeurs séparés. Les deux processeurs lisent la même donnée dans le tampon de profondeur et les deux fragments passent le ''z-test'', car ils n'ont aucun moyen de savoir la coordonnée z en cours de traitement dans l'autre processeur. Les deux processeurs vont alors écrire leur résultat en mémoire et c'est premier arrivé, premier servi. Le résultat n'est pas forcément celui attendu : le pixel le plus proche peut être écrit avant le plus lointain, ou inversement. [[File:Situation où faire le z-test dans les pixel shaders dysfonctionne.png|centre|vignette|upright=2|Situation où faire le z-test dans les pixel shaders dysfonctionne]] Pour obtenir un bon rendu, le GPU doit forcer le z-test à se faire fragment par fragment, du moins quand on regarde un pixel individuel. Il reste possible de traiter des pixels différents en parallèle, mais pas deux fragments d'un même pixel. En utilisant des processeurs de shaders qui travaillent en parallèle, cette contrainte est parfois brisée et le rendu donne des résultats incorrects. Le tampon de profondeur n'est pas conçu pour être parallélisé, idem pour le mélange ''alpha''. Il faut donc une sorte de point de synchronisation dans le pipeline pour éviter tout problème. Et c'est à ça que servent les ROPs. Une solution alternative serait de mémoriser, pour chaque pixel, si un ''pixel shader'' est en train de le traiter. Il suffit de mémoriser un bit par pixel pour cela, dans une table d'utilisation, concrètement une petite mémoire. Elle serait mise à jour par les processeurs de shaders, et consultée par le rastériseur. Quand le rastériseur génère un fragment, il consulte cette table, pour vérifier s'il y a conflit avec les fragments en cours de traitement. Il attend si c'est le cas, le pixel shader finira par finir de traiter le pixel au bout d'un moment. Mais l'inconvénient de cette solution est qu'elle a besoin d'une mémoire partagée par tous les processeurs de shaders, qui est difficile à concevoir sans faire des concessions en termes de performances. Une autre solution serait de mémoriser tous les pixels en cours de traitement. Quand le rastériseur génère un fragment, il mémorise les coordonnées x,y de ce fragment à l'écran, dans une '''table des pixels occupés'''. Dès qu'un pixel shader se termine, la table des pixels occupés est mise à jour. Le rastériseur consulte cette table quand il génère un fragment, afin de détecter les conflits. S'il y a conflit, le rastériseur attend que le fragment conflictuel, en cours de traitement dans le pixel shader, soit traité. L’inconvénient est que la table des pixels occupés est techniquement une mémoire associative, une sorte de mémoire cache, qui est plus complexe qu'une simple RAM. Il est très difficile de créer une mémoire de ce genre qui soit capable de mémoriser plusieurs milliers de pixels, pour gérer une centaine de processeurs de shaders. En pratique, les cartes graphiques pour PC ont des ROPs séparés des processeurs de ''shaders''. Par contre, quelques cartes graphiques destinées les smartphones et autres appareils mobiles font exception. Sur celles-ci, les ROPs sont intégrés dans les processeurs de ''shaders'', aux côtés de l'unité d'accès mémoire LOAD/STORE. Il s'agit de cartes graphiques en rendu à tuiles, qui n'ont que très peu de processeurs de shader. {{NavChapitre | book=Les cartes graphiques | prev=Les cartes graphiques : architecture de base | prevText=Les cartes graphiques : architecture de base | next=Les processeurs de shaders | nextText=Les processeurs de shaders }} {{autocat}} gl62tvctuiwlo1maime238yrbsuzwir 763510 763509 2026-04-11T20:58:41Z Mewtow 31375 /* Les ROPs peuvent être implémentés dans le pixel shader */ 763510 wikitext text/x-wiki Il est intéressant d'étudier le hardware des cartes graphiques en faisant un petit résumé de leur évolution dans le temps. En effet, leur hardware a fortement évolué dans le temps. Et il serait difficile à comprendre le hardware actuel sans parler du hardware d'antan. En effet, une carte graphique moderne est partiellement programmable. Certains circuits sont totalement programmables, d'autres non. Et pour comprendre pourquoi, il faut étudier comment ces circuits ont évolués. Le hardware des cartes graphiques a fortement évolué dans le temps, ce qui n'est pas une surprise. Les évolutions de la technologie, avec la miniaturisation des transistors et l'augmentation de leurs performances a permis aux cartes graphiques d'incorporer de plus en plus de circuits avec les années. Avant l'invention des cartes graphiques, toutes les étapes du pipeline graphique étaient réalisées par le processeur : il calculait l'image à afficher, et l’envoyait à une carte d'affichage 2D. Au fil du temps, de nombreux circuits furent ajoutés, afin de déporter un maximum de calculs vers la carte vidéo. Le rendu 3D moderne est basé sur le placage de texture inverse, avec des coordonnées de texture, une correction de perspective, etc. Mais les anciennes consoles et bornes d'arcade utilisaient le placage de texture direct. Et cela a impacté le hardware des consoles/PCs de l'époque. Avec le placage de texture direct, il était primordial de calculer la géométrie, mais la rasterisation était le fait de VDC améliorés. Aussi, les premières bornes d'arcade 3D et les consoles de 5ème génération disposaient processeurs pour calculer la géométrie et de circuits d'application de textures très particuliers. A l'inverse, les PC utilisaient un rendu inverse, totalement différent. Sur les PC, les premières cartes graphiques avaient un circuit de rastérisation et des unités de textures, mais pas de circuits géométriques. ==Les premières cartes graphiques== Dès les années 70-80, le rendu 3D était utilisé par de nombreuses entreprises industrielles : des applications de visualisation 3D étaient utilisées en architecture, des applications de conception assistée par ordinateur étaient déjà d'utilisation courante, sans compter les simulateurs de vol utilisés par l'armée et les instructeurs qui formaient les pilotes d'avion. Le rendu 3D était aussi étudié au niveau académique, la recherche en 3D était déjà florissante. Il existait même du matériel spécifiquement conçu pour le rendu graphique, mais celui-ci était spécifiquement dédié à des super-calculateurs ou des ''workstations'' (des sortes d'ancêtres des PC, très puissants pour l'époque, mais conçus uniquement pour les entreprises). ===Le début des années 80 : le rendu en fils de fer=== Le tout premier système de ce genre était le '''''Line Drawing System-1''''' de l'entreprise Evans & Sutherland, daté de 1969. Ce n'est ni plus ni moins que le toute premier circuit graphique séparé du processeur ayant existé. C'est en un sens la toute première carte graphique, le tout premier GPU. Il prenait la forme d'un périphérique qui se connectait à l'ordinateur d'un côté et était relié à l'écran de l'autre. Il était compatible avec un grand nombre d'ordinateurs et de processeurs existants. Il a été suivi par plusieurs successeurs, nommés ''Picture System 1, 2'' et le ''PS300 series''. [[File:Evans & Sutherland LDS-1 (1).jpg|vignette|Evans & Sutherland LDS-1 (1)]] Ils permettaient de faire du rendu en fil de fer, sans texture ni même sans polygones colorés. Un tel rendu était utile pour des applications assez limitées : architecture, dessin de molécules pour les entreprises pharmaceutique et certains centres de recherche, l'aérospatiale, etc. Ces cartes graphiques étaient utilisées de concert avec des écrans appelés '''écrans vectoriels''' (''vector display''). Pour simplifier, ils ressemblaient à des écrans CRT, sauf que le faisceau d'électron ne balayait pas l'écran ligne par ligne, mais traçait des lignes arbitraires à l'écran. On lui précisait deux points de coordonnées x1,y1 ; et x2,y2 ; puis l'écran tracait une ligne entre ces deux points. En général, la ligne tracée était maintenue pendant un long moment, entre plusieurs secondes et plusieurs minutes. L'intérieur du circuit était assez simple : un circuit de multiplication de matrice pour les calculs géométriques, un rastériser simplifié (le ''clipping diviser''), un circuit de tracé de lignes, et un processeur de contrôle pour commander les autres circuits. Le fait que ces trois circuits soient séparés permettait une implémentation en pipeline, où plusieurs portions de l'image pouvaient être calculées en même temps : pendant que l'une est dans l'unité géométrique, l'autre est dans le rastériseur et une troisième est en cours de tracé. [[File:Lds1blockdiagram05.svg|centre|vignette|upright=2|Architecture du LDS-1. Le processeur de contrôle n'est pas représenté.]] Le processeur de contrôle exécute un programme qui se charge de commander l'unité géométrique et les autres circuits. Le programme en question est fourni par le programmeur, le LDS-1 est donc totalement programmable. Il lit directement les données nécessaires pour le rendu dans la mémoire de l’ordinateur et le programme exécuté est lui aussi en mémoire principale. Il n'a pas de mémoire vidéo dédiée, il utilise la RAM de l'ordinateur principal. Le multiplieur de matrices est plus complexe qu'on pourrait s'y attendre. Il ne s'agit pas que d'un circuit arithmétique tout simple, mais d'un véritable processeur avec des registres et des instructions machine complexes. Il contient plusieurs registres, l'ensemble mémorisant 4 matrices de 16 nombres chacune (4 lignes de 4 colonnes). Un nombre est codé sur 18 bits. Les registres sont reliés à un ensemble de circuits arithmétiques, des additionneurs et des multiplieurs. Le circuit supporte des instructions de copie entre registres, pour copier une ligne d'une matrice à une autre, des instructions LOAD/STORE pour lire ou écrire dans la mémoire RAM, etc. Il supporte aussi des multiplications en 2D et 3D. Le ''clipping divider'' est un circuit assez complexe, contenant un processeur à accumulateur, une mémoire ROM pour le programme du processeur. Le programme exécuté par le processeur est un petit programme de 62 instructions, stocké dans la ROM. L'algorithme du ''clipping divider'' est décrite dans le papier de recherche "A clipping divider", écrit par Robert Sproull. Un détail assez intéressant est que le résultat en sortie de l'unité géométrique et du rastériseur peuvent être envoyés à l'ordinateur en parallèle du rendu. C'était très utile sur les anciens ordinateurs qui étaient connectés à plusieurs terminaux. Le LDS-1 calculait la géométrie et le rendu, et le tout pouvait petre envoyé à d'autres composants, comme des terminaux, une imprimante, etc. ===Les systèmes ultérieurs : rendu à triangles colorés et texturé=== Les systèmes précédents étaient très limités : ils calculaient la géométrie et n'avaient pas de ''framebuffer'', ni de tampon de profondeur, ni gestion de l'éclairage, ni quoique ce soit. De tels systèmes étaient donc des accélérateurs géométriques que de vrais systèmes graphiques complets, du fait de l'absence de ''framebuffer''. Ils étaient composés de processeurs spécialisés dans les calculs à virgule flottante, faisant des calculs géométriques, et éventuellement d'un processeur pour la rastérisation. La raison est que la RAM était très chère et que créer des circuits fixes étaient très chers et peu disponibles. Par contre, les processeurs à virgule flottante étaient peu chers et facile à trouver. Vers la fin des années 80, grâce à la baisse du prix de la RAM et la démocratisation des ASIC (des circuits fixes fait sur mesure), ajouter un ''framebuffer'' est est devenu possible. C'est alors que sont apparus les '''systèmes de rendu 3D de première génération'''. De tels systèmes ont permis d'implémenter le rendu à primitives colorées qu'on a vu il y a quelques chapitres, à savoir un rendu où les triangles sont coloriés avec une couleur unique. Les systèmes de première génération étaient simples : des processeurs pour le calcul de la géométrie, un circuit de rastérisation, une RAM pour le ''framebuffer'' et des ASIC servant de ROPs très simples. Il n'y avait pas d'élimination des pixels cachés, pas de textures, et encore moins d'éclairage par pixels. Le premier système de ce genre était le ''Shaded Picture System'', toujours par Evans & Sutherland. Il ne gérait pas la couleur et ne pouvait afficher que des images en noir et blanc, mais il gérait l'éclairage par sommet (''vertex lighting''). Il a rapidement été dépassé par les systèmes de l'entreprise ''Silicon Graphics Inc'' (SGI), ainsi que ceux de l'entreprise Apollo avec sa série Apollo DN. Les '''systèmes de seconde génération''' sont apparus vers la fin des années 80, et se distinguent des précédents par l'ajout un tampon de profondeur. Ils intègrent aussi des capacités d'éclairage par pixel, à savoir de l'éclairage plat, de Gouraud, voire de Phong ! Enfin, les '''systèmes de troisième génération''' ont acquis des capacités de placage de texture, que les systèmes précédents n'avaient pas. Ils ont aussi ajouté un support de l'antialiasing. Les systèmes SGI avec placage de texture ont déjà été abordé au chapitre précédent, dans la section sur les GPU en mode immédiat et à ''tile''. Aussi, nous ne reviendrons pas dessus. [[File:Evolution de l'architecture des premières cartes graphiques, dans les années 80-90.png|centre|vignette|upright=2.5|Evolution de l'architecture des premières cartes graphiques, dans les années 80-90]] Les systèmes de première, seconde et troisième génération avaient de nombreux points communs. En premier lieu, ils étaient fabriqués en connectant plusieurs cartes électroniques : une carte pour les calculs géométriques, une ou plusieurs cartes pour le reste du rendu graphique, une carte dédiée au VDC et avec un connecteur écran. Les transistors de l'époque n'étaient pas encore miniaturisés, ce qui fait que le système graphique ne pouvait pas tenir sur une seule carte électronique. Il n'y avait donc pas de carte graphique proprement dit, mais un équivalent éclaté sur plusieurs cartes électroniques. La carte pour la géométrie contenait typiquement une mémoire FIFO pour accumuler les commandes de rendu, un processeur de commande, et plusieurs processeurs géométriques. Les processeurs géométriques étaient parfois conçus sur mesure, comme l'a été le le ''Geometry Engine'' de SGI. Mais il est arrivé qu'ils utilisent des processeurs commerciaux comme le Weitek 3222, l'Intel i860, etc. Les processeurs pouvaient être placés en série ou en parallèle, comme expliqué dans le chapitre précédent. Le circuit de rastérisation était réalisé soit avec un processeur dédié, soit avec un circuit fixe, soit un mélange des deux. La rastérisation est en effet réalisée en plusieurs étapes, certaines peuvent être implémentées avec un processeur et d'autres avec des circuits fixes. Un point important est qu'à l'époque, le rendu n'utilisait pas que des triangles, mais des polygones en général. Ce n'est que par la suite que le rendu s'est focalisé sur les triangles et les ''quads'' (quadrilatères). Il arrivait que le système graphique gérait partiellement des polygones concaves, voire convexes. Sur les systèmes SGI, les calculs géométriques se faisaient avec des polygones, que la rastérisation découpait en triangles, le reste du rendu se faisait avec des triangles. Les stations de travail Apollo DN 10000VS découpaient les polygones en trapézoïdes orientés à l'horizontale, alignés avec des ''scanlines''. D'autres systèmes découpaient tout en triangle lors de l'étape géométrique ==Les précurseurs grand public : les bornes d'arcade== [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] L'accélération du rendu 3D sur les bornes d'arcade était déjà bien avancé dès les années 90. Les bornes d'arcade ont toujours été un segment haut de gamme de l'industrie du jeu vidéo, aussi ce n'est pas étonnant. Le prix d'une borne d'arcade dépassait facilement les 10 000 dollars pour les plus chères et une bonne partie du prix était celui du matériel informatique. Le matériel était donc très puissant et débordait de mémoire RAM comparé aux consoles de jeu et aux PC. La plupart des bornes d'arcade utilisaient du matériel standardisé entre plusieurs bornes. A l'intérieur d'une borne d'arcade se trouve une '''carte de borne d'arcade''' qui est une carte mère avec un ou plusieurs processeurs, de la RAM, une carte graphique, un VDC et pas mal d'autres matériels. La carte est reliée aux périphériques de la borne : joysticks, écran, pédales, le dispositif pour insérer les pièces afin de payer, le système sonore, etc. Le jeu utilisé pour la borne est placé dans une cartouche qui est insérée dans un connecteur spécialisé. Les cartes de bornes d'arcade étaient généralement assez complexes, elles avaient une grande taille et avaient plus de composants que les cartes mères de PC. Chaque carte contenait un grand nombre de chips pour la mémoire RAM et ROM, et il n'était pas rare d'avoir plusieurs processeurs sur une même carte. Et il n'était pas rare d'avoir trois à quatre cartes superposées dans une seule borne. Pour ceux qui veulent en savoir plus, Fabien Sanglard a publié gratuitement un livre sur le fonctionnement des cartes d'arcade CPS System, disponible via ce lien : [https://fabiensanglard.net/b/cpsb.pdf The book of CP System]. Les premières cartes graphiques des bornes d'arcade étaient des cartes graphiques 2D auxquelles on avait ajouté quelques fonctionnalités. Les sprites pouvaient être tournés, agrandit/réduits, ou déformés pour simuler de la perspective et faire de la fausse 3D. Par la suite, le vrai rendu 3D est apparu sur les bornes d'arcade. Dès 1988, la carte d'arcade Namco System 21 et Sega Model 1 géraient les calculs géométriques. Quelques années plus tard, les cartes graphiques se sont mises à supporter un éclairage de Gouraud et du placage de texture. Par exemple, le Namco System 22 et la Sega model 2 supportaient des textures 2D et comme le filtrage de texture (bilinéaire et trilinéaire), le mip-mapping, et quelques autres. Au passage, les cartes graphiques de la Namco System 22 étaient développées en partenariat avec Eans & Sutherland, qui avait commencé à se diversifier dans le marché grand public. Les cartes graphiques de l'époque faisaient les calculs géométriques sur plusieurs processeurs, généralement des processeurs de type DSP (des processeurs spécialisés dans le traitement de signal). Par exemple, la Namco System 2 utilisait 4 DSP de marque Texas Instruments TMS320C25, cadencés à 24,576 MHz. La carte d'arcade Sega Model 1 utilisait quant à elle un DSP spécialisé dans les calculs géométriques. Par la suite, les bornes d'arcade ont réutilisé le hardware des PC et autres consoles de jeux. ==La 3D sur les consoles de quatrième/cinquième génération== Les consoles avant la quatrième génération de console étaient des consoles purement 2D, sans circuits d'accélération 3D. Leur carte graphique était un simple VDC 2D, plus ou moins performant selon la console. Les premières consoles de jeu capables de rendu 3D par elles-mêmes sont les consoles dites de 5ème génération. Il y a diverses manières de classer les consoles en générations, la plus commune place la 3D à la 5ème génération, mais détailler ces controverses quant à ce classement nous amènerait trop loin. Les consoles de génération avaient une architecture assez différente des systèmes antérieurs. Les systèmes SGI et assimilés pouvaient se permettre de couter assez cher, d'utiliser beaucoup de circuits, de prendre beaucoup de place. Les bornes d'arcade sont aussi dans ce cas. Aussi, il n'était pas rare que les cartes 3D de l'époque tiennent sur plusieurs cartes électroniques séparées. Mais une console ne peut pas se permettre ce genre de folies. Aussi, les cartes 3D des consoles de l'époque tenaient dans un seul circuit intégré, comme il est d'usage de nos jours. La conséquence est que certains circuits étaient fortement simplifiés, sur les consoles de cinquième génération. Et cela a impacté l'architecture interne des GPU des consoles. Les systèmes SGI avaient plusieurs processeurs pour calculer la géométrie, couplés à plusieurs unités non-programmables pour les pixels/textures. Les cartes 3D des consoles gardaient cette organisation : processeurs pour la géométrie, circuits fixes pour le reste. Mais elles se débrouillaient souvent avec un seul processeur, voire aucun ! Dans ce dernier cas, la géométrie était calculée sur le processeur principal, le CPU. Les unités pour les pixels étaient aussi moins nombreuses, mais il y en avait plusieurs, pour profiter de l'amplification des pixels. : Les cartes 3D des consoles de jeu utilisaient le placage de texture inverse, avec quelques exceptions qui utilisaient le placage de texture direct. ===Le rendu 3D sur les consoles de quatrième génération : la SNES=== Plus haut, j'ai dit que les consoles de quatrième génération n'avaient pas de carte accélératrice 3D. Pourtant, elles ont connus quelques jeux en vraie 3D. La raison à cela est que la 3D était calculée par un GPU placé dans les cartouches du jeu ! Par exemple, les cartouches de Starfox et de Super Mario 2 contenaient un coprocesseur Super FX, qui gérait des calculs de rendu 2D/3D. En tout, il y a environ 16 coprocesseurs pour la SNES et on en trouve facilement la liste sur le net. La console était conçue pour, des pins sur les ports cartouches étaient prévues pour des fonctionnalités de cartouche annexes, dont ces coprocesseurs. Ces pins connectaient le coprocesseur au bus des entrées-sorties. Les coprocesseurs des cartouches de NES avaient souvent de la mémoire rien que pour eux, qui était intégrée dans la cartouche. Ceci étant dit, passons aux consoles de cinquième génération. ===La Nintendo 64 : un GPU avancé=== La Nintendo 64 avait le GPU le plus complexe comparé aux autres consoles, et dépassait même les cartes graphiques des PC. Il faut dire que son GPU a été conçu avec l'aide de l'entreprise SGI, dont on a vu les systèmes graphiques plus haut. Le GPU de la N64 incorporait une unité pour les calculs géométriques, un circuit de rasterisation, une unité de textures et un ROP final pour les calculs de transparence/brouillard/antialiasing, ainsi qu'un circuit pour gérer la profondeur des pixels. En somme, tout le pipeline graphique était implémenté dans le GPU de la Nintendo 64, chose très en avance sur son temps, comparé au PC ou aux autres consoles ! Le GPU est construit autour d'un processeur dédié aux calculs géométriques, le ''Reality Signal Processor'' (RSP), autour duquel on a ajouté des circuits pour le reste du pipeline graphique. L'unité de calcul géométrique est un processeur MIPS R4000, un processeur assez courant à l'époque, auquel on avait retiré quelques fonctionnalités inutiles pour le rendu 3D. Il était couplé à 4 KB de mémoire vidéo, ainsi qu'à 4 KB de mémoire ROM. Le reste du GPU était réalisé avec des circuits fixes. Un point intéressant est que le programme exécuté par le RSP pouvait être programmé ! Le RSP gérait déjà des espèces de proto-shaders, qui étaient appelés des ''[https://ultra64.ca/files/documentation/online-manuals/functions_reference_manual_2.0i/ucode/microcode.html micro-codes]'' dans la documentation de l'époque. La ROM associée au RSP mémorise cinq à sept programmes différents, aux fonctionnalités différentes. * Les microcodes gspFast3D et gspF3DNoN, implémentent un rendu 3D normal, avec des options de ''clipping'' différentes entre les deux. * Le microcode gspTurbo3D fait la même chose, mais avec moins de fonctionnalités et avec une précision réduite. Il ne gère pas le ''clipping'', l'éclairage par pixel, la correction de perspective, l'antialiasing et quelques autres fonctionnalités. Il gère cependant l'éclairage de Gouraud. Il utilise une ''display list'' simplifiée comparé aux deux microcodes précédents. * Le microcode gspZ-Sort effectue une pré-passe z, à savoir qu'il calcule le tampon de profondeur final de la scène 3D, sans rendre l'image. Cela sert à faire une élimination des pixels cachés parfaite, en logiciel. On calcule le tampon de profondeur pour déterminer quels pixels sont visibles, puis une seconde passe rend l'image en, rejetant les pixels non-visibles. * Le microcode gspSprite2D implémente un rendu 2D émulé : les sprites et arrière-plan sont des rectangles texturés. Le microcode gspS2DEX fait la même chose, mais sert à émuler le rendu de la SNES plus qu'autre chose. * Le microcode gspLine3D ne gére que des lignes, pas de triangles. Il sert pour du rendu en fil de fer. Ils géraient le rendu 3D de manière différente et avec une gestion des ressources différentes. Très peu de studios de jeu vidéo ont développé leur propre microcodes N64, car la documentation était mal faite, que Nintendo ne fournissait pas de support officiel pour cela, que les outils de développement ne permettaient pas de faire cela proprement et efficacement. ===La Playstation 1=== Sur la Playstation 1 le calcul de la géométrie était réalisé par le processeur, la carte graphique gérait tout le reste. Et la carte graphique était un circuit fixe spécialisé dans la rasterisation et le placage de textures. Elle utilisait, comme la Nintendo 64, le placage de texture inverse, qui est apparu ensuite sur les cartes graphiques. ===La 3DO et la Sega Saturn=== La Sega Saturn et la 3DO étaient les deux seules consoles à utiliser le rendu direct. La géométrie était calculée sur le processeur, même si les consoles utilisaient parfois un CPU dédié au calcul de la géométrie. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. La Sega Saturn incorpore trois processeurs et deux GPU. Les deux GPUs sont nommés le VDP1 et le VDP2. Le VDP1 s'occupe des textures et des sprites, le VDP2 s'occupe uniquement de l'arrière-plan et incorpore un VDC tout ce qu'il y a de plus simple. Ils ne gèrent pas du tout la géométrie, qui est calculée par les trois processeurs. Le troisième processeur, la Saturn Control Unit, est un processeur de type DSP, à savoir un processeur spécialisé dans le traitement de signal. Il est utilisé presque exclusivement pour accélérer les calculs géométriques. Il avait sa propre mémoire RAM dédiée, 32 KB de SRAM, soit une mémoire locale très rapide. Les transferts entre cette RAM et le reste de l'ordinateur était géré par un contrôleur DMA intégré dans le DSP. En somme, il s'agit d'une sorte de processeur spécialisé dans la géométrie, une sorte d'unité géométrique programmable. Mais la géométrie n'était pas forcément calculée que sur ce DSP, mais pouvait être prise en charge par les 3 CPU. ==L'historique des cartes graphiques des PC, avant l'arrivée de Direct X et Open Gl== Sur PC, l'évolution des cartes graphiques a eu du retard par rapport aux consoles. Les PC sont en effet des machines multi-usage, pour lesquelles le jeu vidéo était un cas d'utilisation parmi tant d'autres. Et les consoles étaient la plateforme principale pour jouer à des jeux vidéo, le jeu vidéo PC étant plus marginal. Mais cela ne veut pas dire que le jeu PC n'existait pas, loin de là ! Un problème pour les jeux PC était que l'écosystème des PC était aussi fragmenté en plusieurs machines différentes : machines Apple 1 et 2, ordinateurs Commdore et Amiga, IBM PC et dérivés, etc. Aussi, programmer des jeux PC n'était pas mince affaire, car les problèmes de compatibilité étaient légion. C'est seulement quand la plateforme x86 des IBM PC s'est démocratisée que l'informatique grand public s'est standardisée, réduisant fortement les problèmes de compatibilité. Mais cela n'a pas suffit, il a aussi fallu que les API 3D naissent. Les API 3D comme Direct X et Open GL sont absolument cruciales pour garantir la compatibilité entre plusieurs ordinateurs aux cartes graphiques différentes. Aussi, l'évolution des cartes graphiques pour PC s'est faite main dans la main avec l'évolution des API 3D. Les fonctionnalités des cartes graphiques ont évolué dans le temps, en suivant les évolutions des API 3D. Du moins dans les grandes lignes, car il est arrivé plusieurs fois que des fonctionnalités naissent sur les cartes graphiques, pour que les fabricants forcent la main de Microsoft ou d'Open GL pour les intégrer de force dans les API 3D. Passons. ===L'introduction des premiers jeux 3D : Quake et les drivers miniGL=== L'API OpenGL est née de la main de SGI, encore eux ! SGI avait créé l'API Iris GL pour ses stations de travail Iris Graphics. Iris GL a ensuite été libéré et est devenu le standard Open GL. Open GL existait déjà avant l'apparition des cartes accélératrices 3D. Il y a avait donc déjà un terreau que les programmeurs graphiques pouvaient utiliser. Mais Open GL était surtout utilisé pour des applications industrielles, médicales (imagerie), graphiques ou militaires, pas pour le jeu vidéo. Mais cela changea avec la sortie du jeu Quake, d'IdSoftware, en 1996. Quake pouvait fonctionner en rendu logiciel, mais le programmeur responsable du moteur 3D (le célébre John Carmack) ajouta une version OpenGL du jeu. Il faut dire que le jeu était programmé sur une station de travail compatible avec OpenGL, même si aucune carte accélératrice de l'époque ne supportait OpenGL. C'était là un choix qui se révéla visionnaire. En théorie, le rendu par OpenGL aurait dû se faire intégralement en logiciel, sauf sur quelques rares stations de travail adaptées. Mais les premières cartes graphiques étaient déjà dans les starting blocks. La toute première carte 3D pour PC est la '''Rendition Vérité V1000''', sortie en Septembre 1995, soit quelques mois avant l'arrivée de la Nintendo 64. La Rendition Vérité V1000 contenait un processeur MIPS cadencé à 25 MHz, 4 mébioctets de RAM, une ROM pour le BIOS, et un RAMDAC, rien de plus. C'était un vrai ordinateur complètement programmable de bout en bout, sans aucun circuit fixe. Les programmeurs ne pouvaient cependant pas utiliser cette programmabilité avec des ''shaders'', mais elle permettait à Rendition d'implémenter n'importe quelle API 3D, que ce soit OpenGL, DirectX ou même sa son API propriétaire. La Rendition Vérité avait de bonnes performances pour ce qui est de la géométrie, mais pas pour le reste. Réaliser la rastérisation et le placage de texture en logiciel n'est pas efficace, pareil pour les opérations de fin de pipeline comme l'antialiasing. Le manque d'unités fixes très rapides pour la rastérisation, le placage de texture ou les opérations de fin de pipeline était clairement un gros défaut. Mais la Rendition Vérité était un cas à part, une exception dans le paysage des cartes 3D de l'époque, qui ne faisait rien comme les autres. Les autres cartes graphiques, sorties peu après, étaient les Voodoo de 3dfx, les Riva TNT de NVIDIA, les Rage/3D d'ATI, la Virge/3D de S3, et la Matrox Mystique. Elles avaient choisit le compromis inverse de la Rendition Vérité V1000 : de bonnes performances pour le placage de textures et la rastérization, mais pas pour les calculs géométriques. Pour rappel, les systèmes professionnels et les consoles avaient des processeurs pour la géométrie, et des circuits fixes pour le reste. Les cartes graphiques de PC se passaient des processeurs pour la géométrie, les calculs géométriques étaient réalisés par le CPU. Les toutes premières cartes 3D pour PC contenaient seulement des circuits pour gérer les textures et des ROPs. Elle géraient le ''z-buffer'' en mémoire vidéo, ainsi que des effets de brouillard. Il n'y avait même pas de circuit pour la rastérisation, qui était faite en logiciel, avec les calculs géométriques. [[File:Architecture de base d'une carte 3D - 2.png|centre|vignette|upright=1.5|Carte 3D sans rasterization matérielle.]] Les cartes suivantes ajoutèrent une gestion des étapes de ''rasterization'' directement en matériel. Les cartes ATI rage 2, les Invention de chez Rendition, et d'autres cartes graphiques supportaient la rasterisation en hardware. [[File:Architecture de base d'une carte 3D - 3.png|centre|vignette|upright=1.5|Carte 3D avec gestion de la géométrie.]] Pour exploiter les unités de texture et le circuit de rastérisation, OpenGL et Direct 3D étaient partiellement implémentées en logiciel, car les cartes graphiques ne supportaient pas toutes les fonctionnalités de l'API. C'était l'époque du miniGL, des implémentations partielles d'OpenGL, fournies par les fabricants de cartes 3D, implémentées dans les pilotes de périphériques de ces dernières. Les fonctionnalités d'OpenGL implémentées dans ces pilotes étaient presque toutes exécutées en matériel, par la carte graphique. Avec l'évolution du matériel, les pilotes de périphériques devinrent de plus en plus complets, au point de devenir des implémentations totales d'OpenGL. Mais au-delà d'OpenGL, chaque fabricant de carte graphique avait sa propre API propriétaire, qui était gérée par leurs pilotes de périphériques (''drivers''). Par exemple, les premières cartes graphiques de 3dfx interactive, les fameuses voodoo, disposaient de leur propre API graphique, l'API Glide. Elle facilitait la gestion de la géométrie et des textures, ce qui collait bien avec l'architecture de ces cartes 3D. Mais ces API propriétaires tombèrent rapidement en désuétude avec l'évolution de DirectX et d'OpenGL. Direct X était une API dans l'ombre d'Open GL. La première version de Direct X qui supportait la 3D était DirectX 2.0 (juin 2, 1996), suivie rapidement par DirectX 3.0 (septembre 1996). Elles dataient d'avant le jeu Quake, et elles étaient très éloignées du hardware des premières cartes graphiques. Elles utilisaient un système d'''execute buffer'' pour communiquer avec la carte graphique, Microsoft espérait que le matériel 3D implémenterait ce genre de système. Ce qui ne fu pas le cas. Direct X 4.0 a été abandonné en cours de développement pour laisser à une version 5.0 assez semblable à la 2.0/3.0. Le mode de rendu laissait de côté les ''execute buffer'' pour coller un peu plus au hardware de l'époque. Mais rien de vraiment probant comparé à Open GL. Même Windows utilisait Open GL au lieu de Direct X maison... C'est avec Direct X 6.0 que Direct X est entré dans la cours des grands. Il gérait la plupart des technologies supportées par les cartes graphiques de l'époque. ===Le ''multi-texturing'' de l'époque Direct X 6.0 : combiner plusieurs textures=== Une technologie très importante standardisée par Dirext X 6 est la technique du '''''multi-texturing'''''. Avec ce qu'on a dit dans le chapitre précédent, vous pensez sans doute qu'il n'y a qu'une seule texture par objet, qui est plaquée sur sa surface. Mais divers effet graphiques demandent d'ajouter des textures par dessus d'autres textures. En général, elles servent pour ajouter des détails, du relief, sur une surface pré-existante. Un exemple intéressant vient des jeux de tir : ajouter des impacts de balles sur les murs. Pour cela, on plaque une texture d'impact de balle sur le mur, à la position du tir. Il s'agit là d'un exemple de '''''decals''''', des petites textures ajoutées sur les murs ou le sol, afin de simuler de la poussière, des impacts de balle, des craquelures, des fissures, des trous, etc. Les textures en question sont de petite taille et se superposent à une texture existante, plus grande. Rendre des ''decals'' demande de pouvoir superposer deux textures. Direct X 6.0 supportait l'application de plusieurs textures directement dans le matériel. La carte graphique devait être capable d'accéder à deux textures en même temps, ou du moins faire semblant que. Pour cela, elle doublaient les unités de texture et adaptaient les connexions entre unités de texture et mémoire vidéo. La mémoire vidéo devait être capable de gérer plusieurs accès mémoire en même temps et devait alors avoir un débit binaire élevé. [[File:Multitexturing.png|centre|vignette|upright=2|Multitexturing]] La carte graphique devait aussi gérer de quoi combiner deux textures entre elles. Par exemple, pour revenir sur l'exemple d'une texture d'impact de balle, il faut que la texture d'impact recouvre totalement la texture du mur. Dans ce cas, la combinaison est simple : la première texture remplace l'ancienne, là où elle est appliquée. Mais les cartes graphiques ont ajouté d'autres combinaisons possibles, par exemple additionner les deux textures entre elle, faire une moyenne des texels, etc. Les opérations pour combiner les textures était le fait de circuits appelés des '''''combiners'''''. Concrètement, les ''combiners'' sont de simples unités de calcul. Les ''conbiners'' ont beaucoup évolués dans le temps, mais les premières implémentation se limitaient à quelques opérations simples : addition, multiplication, superposition, interpolation. L'opération effectuer était envoyée au ''conbiner'' sur une entrée dédiée. [[File:Multitexturing avec combiners.png|centre|vignette|upright=2|Multitexturing avec combiners]] S'il y avait eu un seul ''conbiner'', le circuit de ''multitexturing'' aurait été simplement configurable. Mais dans la réalité, les premières cartes utilisant du ''multi-texturing'' utilisaient plusieurs ''combiners'' placés les uns à la suite des autres. L'implémentation des ''combiners'' retenue par Open Gl, et par le hardware des cartes graphiques, était la suivante. Les ''combiners'' étaient placés en série, l'un à la suite de l'autre, chacun combinant le résultat de l'étage précédent avec une texture. Le premier ''combiner'' gérait l'éclairage par sommet, afin de conserver un minimum de rétrocompatibilité. [[File:Texture combiners Open GL.png|centre|vignette|upright=2|Texture combiners Open GL]] Voici les opérations supportées par les ''combiners'' d'Open GL. Ils prennent en entrée le résultat de l'étage précédent et le combinent avec une texture lue depuis l'unité de texture. {|class="wikitable" |+ Opérations supportées par les ''combiners'' d'Open GL |- ! Replace | colspan="2" | Pixel provenant de l'unité de texture |- ! Addition | colspan="2" | Additionne l'entrée au texel lu. |- ! Modulate | colspan="2" | Multiplie l'entrée avec le texel lu |- ! Mélange (''blending'') | Moyenne pondérée des deux entrées, pondérée par la composante de transparence || La couleur de transparence du texel lu et de l'entrée sont multipliées. |- ! Decals | Moyenne pondérée des deux entrées, pondérée par la composante de transparence. || La transparence du résultat est celle de l'entrée. |} Il faut noter qu'un dernier étage de ''combiners'' s'occupait d'ajouter la couleur spéculaire et les effets de brouillards. Il était à part des autres et n'était pas configurable, c'était un étage fixe, qui était toujours présent, peu importe le nombre de textures utilisé. Il était parfois appelé le '''''combiner'' final''', terme que nous réutiliserons par la suite. Mine de rien, cela a rendu les cartes graphiques partiellement programmables. Le fait qu'il y ait des opérations enchainées à la suite, opérations qu'on peut choisir librement, suffit à créer une sorte de mini-programme qui décide comment mélanger plusieurs textures. Mais il y avait une limitation de taille : le fait que les données soient transmises d'un étage à l'autre, sans détours possibles. Par exemple, le troisième étage ne pouvait avoir comme seule opérande le résultat du second étage, mais ne pouvait pas utiliser celui du premier étage. Il n'y avait pas de registres pour stocker ce qui sortait de la rastérisation, ni pour mémoriser temporairement les texels lus. ===Le ''Transform & Lighting'' matériel de Direct X 7.0=== [[File:Architecture de base d'une carte 3D - 4.png|vignette|upright=1.5|Carte 3D avec gestion de la géométrie.]] La première carte graphique pour PC capable de gérer la géométrie en hardware fût la Geforce 256, la toute première Geforce. Son unité de gestion de la géométrie n'est autre que la bien connue '''unité T&L''' (''Transform And Lighting''). Elle implémentait des algorithmes d'éclairage de la scène 3D assez simples, comme un éclairage de Gouraud, qui étaient directement câblés dans ses circuits. Mais contrairement à la Nintendo 64 et aux bornes d'arcade, elle implémentait le tout, non pas avec un processeur classique, mais avec des circuits fixes. Avec Direct X 7.0 et Open GL 1.0, l'éclairage était en théorie limité à de l'éclairage par sommet, l'éclairage par pixel n'était pas implémentable en hardware. Les cartes graphiques ont tenté d'implémenter l'éclairage par pixel, mais cela n'est pas allé au-delà du support de quelques techniques de ''bump-mapping'' très limitées. Par exemple, Direct X 6.0 implémentait une forme limitée de ''bump-mapping'', guère plus. Un autre problème est qu'il a beaucoup d'algorithmes d'éclairages différents, aux résultats visuels différents, bien au-delà des algorithmes d'éclairage plat, de Gouraud et de Phong. Et les unités de T&L étaient souvent en retard sur les algorithmes logiciels. Les programmeurs avaient le choix entre programmer les algorithmes d’éclairage qu'ils voulaient et les exécuter en logiciel, ou utiliser ceux de l'unité de T&L. Ils choisissaient souvent la première option. Par exemple, Quake 3 Arena et Unreal Tournament n'utilisaient pas les capacités d'éclairage géométrique et préféraient utiliser leurs calculs d'éclairage logiciel fait maison. Cependant, le hardware dépassait les capacités des API et avait déjà commencé à ajouter des capacités de programmation liées au ''multi-texturing''. Les cartes graphiques de l'époque, surtout chez NVIDIA, implémentaient un système de '''''register combiners''''', une forme améliorée de ''texture combiners'', qui permettait de faire une forme limitée d'éclairage par pixel, notamment du vrai ''bump-mampping'', voire du ''normal-mapping''. Mais ce n'était pas totalement supporté par les API 3D de l'époque. Les ''registers combiners'' sont des ''texture combiners'' mais dans lesquels ont aurait retiré la stricte organisation en série. Il y a toujours plusieurs étages à la suite, qui peuvent exécuter chacun une opération, mais tous les étages ont maintenant accès à toutes les textures lues et à tout ce qui sort de la rastérisation, pas seulement au résultat de l'étape précédente. Pour cela, on ajoute des registres pour mémoriser ce qui sort des unités de texture, et pour ce qui sort de la rastérisation. De plus, on ajoute des registres temporaires pour mémoriser les résultats de chaque ''combiner'', de chaque étage. Il faut cependant signaler qu'il existe un ''combiner'' final, séparé des étages qui effectuent des opérations proprement dits. Il s'agit de l'étage qui applique la couleur spéculaire et les effets de brouillards. Il ne peut être utilisé qu'à la toute fin du traitement, en tant que dernier étage, on ne peut pas mettre d'opérations après lui. Sa sortie est directement connectée aux ROPs, pas à des registres. Il faut donc faire la distinction entre les '''''combiners'' généraux''' qui effectuent une opération et mémorisent le résultat dans des registres, et le ''combiner'' final qui envoie le résultat aux ROPs. L'implémentation des ''register combiners'' utilisait un processeur spécialisés dans les traitements sur des pixels, une sorte de proto-processeur de ''shader''. Le processeur supportait des opérations assez complexes : multiplication, produit scalaire, additions. Il s'agissait d'un processeur de type VLIW, qui sera décrit dans quelques chapitres. Mais ce processeur avait des programmes très courts. Les premières cartes NVIDIA, comme les cartes TNT pouvaient exécuter deux opérations à la suite, suivie par l'application de la couleurs spéculaire et du brouillard. En somme, elles étaient limitées à un ''shader'' à deux/trois opérations, mais c'était un début. Le nombre d'opérations consécutives est rapidement passé à 8 sur la Geforce 3. ===L'arrivée des ''shaders'' avec Direct X 8.0=== [[File:Architecture de la Geforce 3.png|vignette|upright=1.5|Architecture de la Geforce 3]] Les ''register combiners'' était un premier pas vers un éclairage programmable. Paradoxalement, l'évolution suivante s'est faite non pas dans l'unité de rastérisation/texture, mais dans l'unité de traitement de la géométrie. La Geforce 3 a remplacé l'unité de T&L par un processeur capable d'exécuter des programmes. Les programmes en question complétaient l'unité de T&L, afin de pouvoir rajouter des techniques d'éclairage plus complexes. Le tout a permis aussi d'ajouter des animations, des effets de fourrures, des ombres par ''shadow volume'', des systèmes de particule évolués, et bien d'autres. À partir de la Geforce 3 de Nvidia, les cartes graphiques sont devenues capables d'exécuter des programmes appelés '''''shaders'''''. Le terme ''shader'' vient de ''shading'' : ombrage en anglais. Grace aux ''shaders'', l'éclairage est devenu programmable, il n'est plus géré par des unités d'éclairage fixes mais été laissé à la créativité des programmeurs. Les programmeurs ne sont plus vraiment limités par les algorithmes d'éclairage implémentés dans les cartes graphiques, mais peuvent implémenter les algorithmes d'éclairage qu'ils veulent et peuvent le faire exécuter directement sur la carte graphique. Les ''shaders'' sont classifiés suivant les données qu'ils manipulent : '''''pixel shader''''' pour ceux qui manipulent des pixels, '''''vertex shaders''''' pour ceux qui manipulent des sommets. Les premiers sont utilisés pour implémenter l'éclairage par pixel, les autres pour gérer tout ce qui a trait à la géométrie, pas seulement l'éclairage par sommets. Direct X 8.0 avait un standard pour les shaders, appelé ''shaders 1.0'', qui correspondait parfaitement à ce dont était capable la Geforce 3. Il standardisait les ''vertex shaders'' de la Geforce 3, mais il a aussi renommé les ''register combiners'' comme étant des ''pixel shaders'' version 1.0. Les ''register combiners'' n'ont pas évolués depuis la Geforce 256, si ce n'est que les programmes sont passés de deux opérations successives à 8, et qu'il y avait possibilité de lire 4 textures en ''multitexturing''. A l'opposé, le processeur de ''vertex shader'' de la Geforce 3 était capable d'exécuter des programmes de 128 opérations consécutives et avait 258 registres différents ! Des ''pixels shaders'' plus évolués sont arrivés avec l'ATI Radeon 8500 et ses dérivés. Elle incorporait la technologie ''SMARTSHADER'' qui remplacait les ''registers combiners'' par un processeur de ''shader'' un peu limité. Un point est que le processeur acceptait de calculer des adresses de texture dans le ''pixel shader''. Avant, les adresses des texels à lire étaient fournis par l'unité de rastérisation et basta. L'avantage est que certains effets graphiques étaient devenus possibles : du ''bump-mapping'' avancé, des textures procédurales, de l'éclairage par pixel anisotrope, du éclairage de Phong réel, etc. Avec la Radeon 8500, le ''pixel shader'' pouvait calculer des adresses, et lire les texels associés à ces adresses calculées. Les ''pixel shaders'' pouvaient lire 6 textures, faire 8 opérations sur les texels lus, puis lire 6 textures avec les adresses calculées à l'étape précédente, et refaire 8 opérations. Quelque chose de limité, donc, mais déjà plus pratique. Les ''pixel shaders'' de ce type ont été standardisé dans Direct X 8.1, sous le nom de ''pixel shaders 1.4''. Encore une fois, le hardware a forcé l'intégration dans une API 3D. ===Les ''shaders'' de Direct X 9.0 : de vrais ''pixel shaders''=== Avec Direct X 9.0, les ''shaders'' sont devenus de vrais programmes, sans les limitations des ''shaders'' précédents. Les ''pixels shaders'' sont passés à la version 2.0, idem pour les ''vertex shaders''. Concrètement, ils ont des fonctionnalités bien supérieures à celles des ''registers combiners''. Les ''shaders'' pouvaient exécuter une suite d'opérations arbitraire, dans le sens où elle n'était pas structurée avec tel type d'opération au début, suivie par un accès aux textures, etc. On pouvait mettre n'importe quelle opération dans n'importe quel ordre. De plus, les ''shaders'' ne sont plus écrit en assembleur comme c'était le cas avant. Ils sont dorénavant écrits dans un langage de haut-niveau, le HLSL pour les shaders Direct X et le GLSL pour les shaders Open Gl. Les ''shaders'' sont ensuite traduit (compilés) en instructions machines compréhensibles par la carte graphique. Au début, ces langages et la carte graphique supportaient uniquement des opérations simples. Mais au fil du temps, les spécifications de ces langages sont devenues de plus en plus riches à chaque version de Direct X ou d'Open Gl, et le matériel en a fait autant. Le matériel s'est alors adapté, en incorporant un véritable processeur pour les ''pixel shaders''. Les ''pixel shaders'' sont maintenant exécutés par un processeur de ''shader'' dédié, aux fonctionnalités bien supérieures à celles des ''registers combiners''. Le processeur de ''pixel shader'' incorpore l'unité de texture en sont sein, les deux sont fusionnés. La raison à cela sera expliqué dans la suite du chapitre. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=1.5|Carte 3D avec pixels et vertex shaders non-unifiés.]] ===L'après Direct X 9.0=== Avant Direct X 10, les processeurs de ''shaders'' ne géraient pas exactement les mêmes opérations pour les processeurs de ''vertex shader'' et de ''pixel shader''. Les processeurs de ''vertex shader'' et de ''pixel shader''étaient séparés. Depuis DirectX 10, ce n'est plus le cas : le jeu d'instructions a été unifié entre les vertex shaders et les pixels shaders, ce qui fait qu'il n'y a plus de distinction entre processeurs de vertex shaders et de pixels shaders, chaque processeur pouvant traiter indifféremment l'un ou l'autre. [[File:Architecture de base d'une carte 3D - 6.png|centre|vignette|upright=1.5|Architecture de la GeForce 6800.]] Avec Direct X 10, de nombreux autres ''shaders'' sont apparus. Les plus liés au rendu 3D sont les '''''geometry shader''''' pour ceux qui manipulent des triangles, de ''hull shaders'' et de ''domain shaders'' pour la tesselation. De plus, les cartes graphiques modernes sont capables d’exécuter des programmes informatiques qui n'ont aucun lien avec le rendu 3D, mais sont exécutés par la carte graphique comme le ferait un processeur d'ordinateur normal. De tels ''shaders'' sans lien avec le rendu 3D sont appelés des ''compute shader''. ==Les cartes graphiques d'aujourd'hui== Les circuits d'un GPU ont beaucoup évolué depuis l'introduction des ''shaders'', pour devenir de plus en plus programmables. Mais à côté des processeurs de ''shaders'', il reste quelques circuits non-programmables appelés des circuits fixes. La rastérisation, le placage de texture, l'élimination des pixels cachés et le mélange ''alpha'' sont gérés par des circuits fixes. [[File:3D-Pipeline.svg|centre|vignette|upright=3.0|Pipeline 3D : ce qui est programmable et ce qui ne l'est pas dans une carte graphique moderne.]] Mais pourquoi ne pas tout rendre programmable ? Ou au contraire, utiliser seulement des circuits fixes ? La réponse rapide est qu'il s'agit d'un compromis entre flexibilité et performance qui permet d'avoir le meilleur des deux mondes. Mais ce compromis a fortement évolué dans le temps, comme on va le voir plus bas. Rendre l'éclairage programmable permet d'implémenter facilement un grand nombre d'effets graphiques sans avoir à les implémenter en hardware. Avant les ''shaders'', les effets graphiques derniers cri n'étaient disponibles que sur les derniers modèles de carte graphique. Avec des ''vertex/pixel shaders'', ce genre de défaut est passé à la trappe. Si un nouvel algorithme de rendu graphique est inventé, il peut être utilisé dès le lendemain sur toutes les cartes graphiques modernes. De plus, implémenter beaucoup d'algorithmes d'éclairage différents avec des circuits fixes a un cout en termes de transistors, alors qu'utiliser des circuits programmable a un cout en hardware plus limité. Tout cela est à l'exact opposé de ce qu'on a avec les autres circuits, comme les circuits pour la rastérisation ou le placage de texture. Il n'y a pas 36 façons de rastériser une scène 3D et la flexibilité n'est pas un besoin important pour cette opération, alors que les performances sont cruciales. Même chose pour le placage/filtrage de textures. En conséquences, les unités de rastérisation, de texture, et les ROPs sont toutes implémentées en matériel. Faire ainsi permet de gagner en performance sans que cela ait le moindre impact pour le programmeur. Reste à expliquer dans le détail pourquoi. ===Les unités de texture sont intégrées aux processeurs de shaders=== Avec l'arrivée des processeurs de shaders, les unités de texture ont été intégrées dans les processeurs de shaders eux-mêmes. C'est la seule unité fixe qui a subit ce traitement, et il est intéressant de comprendre pourquoi. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=2|Architecture de base d'une carte 3D.]] Pour cela, il faut faire un rappel sur ce qu'il y a dans un processeur. Un processeur contient globalement quatre circuits : * une unité de calcul qui fait des calculs ; * des registres pour stocker les opérandes et résultats des calculs ; * une unité de communication avec la mémoire ; * et un séquenceur, un circuit de contrôle qui commande les autres. L'unité de communication avec la mémoire sert à lire ou écrire des données, à les transférer de la RAM vers les registres, ou l'inverse. Lire une donnée demande d'envoyer son adresse à la RAM, qui répond en envoyant la donnée lue. Elle est donc toute indiquée pour lire une texture : lire une texture n'est qu'un cas particulier de lecture de données. Les texels à lire sont à une adresse précise, la RAM répond à la lecture avec le texel demandé. Il est donc possible d'utiliser l'unité de communication avec la mémoire comme si c'était une unité de texture. Cependant, les textures ne sont pas utilisées comme telles de nos jours. Le rendu 3D moderne utilise des techniques dites de filtrage de texture, qui permettent d'améliorer la qualité du rendu des textures. Sans ce filtrage de texture, les textures appliquées naïvement donnent un résultat assez pixelisé et assez moche, pour des raisons assez techniques. Le filtrage élimine ces artefacts, en utilisant une forme d'''antialiasing'' interne aux textures, le fameux filtrage de texture. Le filtrage de texture peut être réalisé en logiciel ou en matériel. Techniquement, il est possible de le faire dans un ''shader''. Le ''shader'' calcule les adresses des texels à lire, lit les texels, et effectue ensuite le filtrage avec des opérations de calcul. Mais ce n'est pas ce qui est fait, le filtrage de texture est toujours effectué directement en matériel. La raison est que le filtrage de texture est très simple à implémenter en hardware. Le filtrage bilinéaire ou trilinéaire demande juste des circuits d'interpolation et quelques registres, ce qui est trivial. Et la seconde raison est qu'il n'y a pas 36 façons de filtrer des textures : une carte graphique peut implémenter les algorithmes principaux existants en assez peu de circuits. Pour simplifier l'implémentation, les processeurs de ''shader'' modernes disposent d'une unité d'accès mémoire séparée de l'unité de texture. L'unité d'accès mémoire normale s'occupe des accès mémoire hors-textures, alors que l'unité mémoire s'occupe de lire les textures. L'unité de texture contient de quoi faire du filtrage de texture, mais aussi faire des calculs d'adresse spécialisées, intrinsèquement liés au format des textures, qu'on détaillera dans le chapitre sur les textures. En comparaison, les unités d'accès mémoire effectuent des calculs d'adresse plus basiques. Un dernier avantage est que l'unité de texture est reliée au cache de texture, alors que l'unité d'accès mémoire est relié au cache L1/L2. ===Les ROPs peuvent être implémentés dans le ''pixel shader''=== Les ROPs effectuent plusieurs opérations basiques, mais les deux plus importantes sont la gestion du tampon de profondeur et de la transparence. Par transparence, on veut parler du mélange ''alpha''. Pour la gestion du tampon de profondeur, on veut parler du ''z-test'', qui compare la profondeur de deux pixels/fragments. Il s'agit d'opérations simples, qu'un processeur de shader peut faire sans problèmes. Par exemple, le ''z-test'' demande de faire plusieurs étapes : * calculer l'adresse du pixel dans le tampon de profondeur ; * lire le pixel dans le tampon de profondeur ; * Faire la comparaison entre profondeurs ; * Si le résultat de la comparaison est okay : ** écrire la nouvelle valeur z dans le tampon de profondeur, et écrire le nouveau pixel dedans. Le mélange ''alpha'' demande lui de : * calculer l'adresse du pixel dans le ''framebuffer'' ; * lire le pixel dans le ''framebuffer'' ; * faire des additions et multiplications pour le mélange ''alpha'' : * écrire le nouveau pixel dans le ''framebuffer''. Pour résumer il faut pouvoir faire : calcul d'adresse, lecture, écriture, addition, multiplication et comparaisons. Et toutes ces opérations sont supportées nativement par les processeurs de shaders, ce sont des instructions communes. Il est donc possible d'émuler les ROPs dans les pixels shaders. En pratique, c'est assez rare, et il y a une bonne explication à cela. Émuler les ROPs dans un ''pixel shader'' est trivial, comme on vient de le voir. Sauf que cela ne marche que si le GPU fait le rendu un pixel à la fois. Le tampon de profondeur est conçu pour traiter un pixel à la fois, idem pour le mélange ''alpha''. Mais si on ne traite pas l'image pixel par pixel, alors les deux algorithmes dysfonctionnent. Donc, tout va bien s'il n'y a qu'un seul processeur de ''pixel shader'', et que celui-ci est conçu pour ne traiter qu'un pixel à la fois, qu'une seule instance de ''shader''. Mais cela ne marche pas sur les GPU modernes, qui ont non seulement près d'une centaine de processeurs de shaders, chacun étant conçu pour traiter une centaine de fragments/pixels en même temps ! Pour donner un exemple, imaginons la situation illustrée ci-dessous. Supposons que l'on ait assez de processeurs de shaders pour traiter plusieurs triangles en même temps. Par malchance, les processeurs rendent en même temps deux triangles opaques qui se recouvrent à l'écran. Là où ils se recouvrent, les deux triangles vont générer deux fragments par pixel, et un seul sera le bon. Pas de chance, les deux fragments sont rendus en parallèle dans deux processeurs séparés. Les deux processeurs lisent la même donnée dans le tampon de profondeur et les deux fragments passent le ''z-test'', car ils n'ont aucun moyen de savoir la coordonnée z en cours de traitement dans l'autre processeur. Les deux processeurs vont alors écrire leur résultat en mémoire et c'est premier arrivé, premier servi. Le résultat n'est pas forcément celui attendu : le pixel le plus proche peut être écrit avant le plus lointain, ou inversement. [[File:Situation où faire le z-test dans les pixel shaders dysfonctionne.png|centre|vignette|upright=2|Situation où faire le z-test dans les pixel shaders dysfonctionne]] Pour obtenir un bon rendu, le GPU doit forcer le z-test à se faire fragment par fragment, du moins quand on regarde un pixel individuel. Il reste possible de traiter des pixels différents en parallèle, mais pas deux fragments d'un même pixel. En utilisant des processeurs de shaders qui travaillent en parallèle, cette contrainte est parfois brisée et le rendu donne des résultats incorrects. Le tampon de profondeur n'est pas conçu pour être parallélisé, idem pour le mélange ''alpha''. Il faut donc une sorte de point de synchronisation dans le pipeline pour éviter tout problème. Et c'est à ça que servent les ROPs. Une solution alternative serait de mémoriser, pour chaque pixel, si un ''pixel shader'' est en train de le traiter. Il suffit de mémoriser un bit par pixel pour cela, dans une table d'utilisation, concrètement une petite mémoire. Elle serait mise à jour par les processeurs de shaders, et consultée par le rastériseur. Quand le rastériseur génère un fragment, il consulte cette table, pour vérifier s'il y a conflit avec les fragments en cours de traitement. Il attend si c'est le cas, le pixel shader finira par finir de traiter le pixel au bout d'un moment. Mais l'inconvénient de cette solution est qu'elle a besoin d'une mémoire partagée par tous les processeurs de shaders, qui est difficile à concevoir sans faire des concessions en termes de performances. Une autre solution serait de mémoriser tous les pixels en cours de traitement. Quand le rastériseur génère un fragment, il mémorise les coordonnées x,y de ce fragment à l'écran, dans une '''table des pixels occupés'''. Dès qu'un pixel shader se termine, la table des pixels occupés est mise à jour. Le rastériseur consulte cette table quand il génère un fragment, afin de détecter les conflits. S'il y a conflit, le rastériseur attend que le fragment conflictuel, en cours de traitement dans le pixel shader, soit traité. L’inconvénient de la solution précédente est que la table des pixels occupés est techniquement une mémoire associative, une sorte de mémoire cache, qui est plus complexe qu'une simple RAM. Il est très difficile de créer une mémoire de ce genre qui soit capable de mémoriser plusieurs dizaines ou centaine de milliers de pixels, pour gérer une centaine de processeurs de shaders. Par contre, elle fonctionne pas trop mal pour un petit nombre de processeurs de shaders, qui fonctionnent à basse fréquence. Cela explique que les GPU pour PC ont des ROPs séparés des processeurs de ''shaders'' : ces GPU ont beaucoup trop de processeurs de shaders. Par contre, quelques cartes graphiques destinées les smartphones et autres appareils mobiles émulent les ROPs dans les ''pixel shaders''. Il s'agit de cartes graphiques en rendu à tuiles, qui n'ont que très peu de processeurs de shader et fonctionnent à basse fréquence. {{NavChapitre | book=Les cartes graphiques | prev=Les cartes graphiques : architecture de base | prevText=Les cartes graphiques : architecture de base | next=Les processeurs de shaders | nextText=Les processeurs de shaders }} {{autocat}} 407wqr7f3dlhtpux00zhcfmi2ciko6g 763511 763510 2026-04-11T21:12:43Z Mewtow 31375 /* Les ROPs peuvent être implémentés dans le pixel shader */ 763511 wikitext text/x-wiki Il est intéressant d'étudier le hardware des cartes graphiques en faisant un petit résumé de leur évolution dans le temps. En effet, leur hardware a fortement évolué dans le temps. Et il serait difficile à comprendre le hardware actuel sans parler du hardware d'antan. En effet, une carte graphique moderne est partiellement programmable. Certains circuits sont totalement programmables, d'autres non. Et pour comprendre pourquoi, il faut étudier comment ces circuits ont évolués. Le hardware des cartes graphiques a fortement évolué dans le temps, ce qui n'est pas une surprise. Les évolutions de la technologie, avec la miniaturisation des transistors et l'augmentation de leurs performances a permis aux cartes graphiques d'incorporer de plus en plus de circuits avec les années. Avant l'invention des cartes graphiques, toutes les étapes du pipeline graphique étaient réalisées par le processeur : il calculait l'image à afficher, et l’envoyait à une carte d'affichage 2D. Au fil du temps, de nombreux circuits furent ajoutés, afin de déporter un maximum de calculs vers la carte vidéo. Le rendu 3D moderne est basé sur le placage de texture inverse, avec des coordonnées de texture, une correction de perspective, etc. Mais les anciennes consoles et bornes d'arcade utilisaient le placage de texture direct. Et cela a impacté le hardware des consoles/PCs de l'époque. Avec le placage de texture direct, il était primordial de calculer la géométrie, mais la rasterisation était le fait de VDC améliorés. Aussi, les premières bornes d'arcade 3D et les consoles de 5ème génération disposaient processeurs pour calculer la géométrie et de circuits d'application de textures très particuliers. A l'inverse, les PC utilisaient un rendu inverse, totalement différent. Sur les PC, les premières cartes graphiques avaient un circuit de rastérisation et des unités de textures, mais pas de circuits géométriques. ==Les premières cartes graphiques== Dès les années 70-80, le rendu 3D était utilisé par de nombreuses entreprises industrielles : des applications de visualisation 3D étaient utilisées en architecture, des applications de conception assistée par ordinateur étaient déjà d'utilisation courante, sans compter les simulateurs de vol utilisés par l'armée et les instructeurs qui formaient les pilotes d'avion. Le rendu 3D était aussi étudié au niveau académique, la recherche en 3D était déjà florissante. Il existait même du matériel spécifiquement conçu pour le rendu graphique, mais celui-ci était spécifiquement dédié à des super-calculateurs ou des ''workstations'' (des sortes d'ancêtres des PC, très puissants pour l'époque, mais conçus uniquement pour les entreprises). ===Le début des années 80 : le rendu en fils de fer=== Le tout premier système de ce genre était le '''''Line Drawing System-1''''' de l'entreprise Evans & Sutherland, daté de 1969. Ce n'est ni plus ni moins que le toute premier circuit graphique séparé du processeur ayant existé. C'est en un sens la toute première carte graphique, le tout premier GPU. Il prenait la forme d'un périphérique qui se connectait à l'ordinateur d'un côté et était relié à l'écran de l'autre. Il était compatible avec un grand nombre d'ordinateurs et de processeurs existants. Il a été suivi par plusieurs successeurs, nommés ''Picture System 1, 2'' et le ''PS300 series''. [[File:Evans & Sutherland LDS-1 (1).jpg|vignette|Evans & Sutherland LDS-1 (1)]] Ils permettaient de faire du rendu en fil de fer, sans texture ni même sans polygones colorés. Un tel rendu était utile pour des applications assez limitées : architecture, dessin de molécules pour les entreprises pharmaceutique et certains centres de recherche, l'aérospatiale, etc. Ces cartes graphiques étaient utilisées de concert avec des écrans appelés '''écrans vectoriels''' (''vector display''). Pour simplifier, ils ressemblaient à des écrans CRT, sauf que le faisceau d'électron ne balayait pas l'écran ligne par ligne, mais traçait des lignes arbitraires à l'écran. On lui précisait deux points de coordonnées x1,y1 ; et x2,y2 ; puis l'écran tracait une ligne entre ces deux points. En général, la ligne tracée était maintenue pendant un long moment, entre plusieurs secondes et plusieurs minutes. L'intérieur du circuit était assez simple : un circuit de multiplication de matrice pour les calculs géométriques, un rastériser simplifié (le ''clipping diviser''), un circuit de tracé de lignes, et un processeur de contrôle pour commander les autres circuits. Le fait que ces trois circuits soient séparés permettait une implémentation en pipeline, où plusieurs portions de l'image pouvaient être calculées en même temps : pendant que l'une est dans l'unité géométrique, l'autre est dans le rastériseur et une troisième est en cours de tracé. [[File:Lds1blockdiagram05.svg|centre|vignette|upright=2|Architecture du LDS-1. Le processeur de contrôle n'est pas représenté.]] Le processeur de contrôle exécute un programme qui se charge de commander l'unité géométrique et les autres circuits. Le programme en question est fourni par le programmeur, le LDS-1 est donc totalement programmable. Il lit directement les données nécessaires pour le rendu dans la mémoire de l’ordinateur et le programme exécuté est lui aussi en mémoire principale. Il n'a pas de mémoire vidéo dédiée, il utilise la RAM de l'ordinateur principal. Le multiplieur de matrices est plus complexe qu'on pourrait s'y attendre. Il ne s'agit pas que d'un circuit arithmétique tout simple, mais d'un véritable processeur avec des registres et des instructions machine complexes. Il contient plusieurs registres, l'ensemble mémorisant 4 matrices de 16 nombres chacune (4 lignes de 4 colonnes). Un nombre est codé sur 18 bits. Les registres sont reliés à un ensemble de circuits arithmétiques, des additionneurs et des multiplieurs. Le circuit supporte des instructions de copie entre registres, pour copier une ligne d'une matrice à une autre, des instructions LOAD/STORE pour lire ou écrire dans la mémoire RAM, etc. Il supporte aussi des multiplications en 2D et 3D. Le ''clipping divider'' est un circuit assez complexe, contenant un processeur à accumulateur, une mémoire ROM pour le programme du processeur. Le programme exécuté par le processeur est un petit programme de 62 instructions, stocké dans la ROM. L'algorithme du ''clipping divider'' est décrite dans le papier de recherche "A clipping divider", écrit par Robert Sproull. Un détail assez intéressant est que le résultat en sortie de l'unité géométrique et du rastériseur peuvent être envoyés à l'ordinateur en parallèle du rendu. C'était très utile sur les anciens ordinateurs qui étaient connectés à plusieurs terminaux. Le LDS-1 calculait la géométrie et le rendu, et le tout pouvait petre envoyé à d'autres composants, comme des terminaux, une imprimante, etc. ===Les systèmes ultérieurs : rendu à triangles colorés et texturé=== Les systèmes précédents étaient très limités : ils calculaient la géométrie et n'avaient pas de ''framebuffer'', ni de tampon de profondeur, ni gestion de l'éclairage, ni quoique ce soit. De tels systèmes étaient donc des accélérateurs géométriques que de vrais systèmes graphiques complets, du fait de l'absence de ''framebuffer''. Ils étaient composés de processeurs spécialisés dans les calculs à virgule flottante, faisant des calculs géométriques, et éventuellement d'un processeur pour la rastérisation. La raison est que la RAM était très chère et que créer des circuits fixes étaient très chers et peu disponibles. Par contre, les processeurs à virgule flottante étaient peu chers et facile à trouver. Vers la fin des années 80, grâce à la baisse du prix de la RAM et la démocratisation des ASIC (des circuits fixes fait sur mesure), ajouter un ''framebuffer'' est est devenu possible. C'est alors que sont apparus les '''systèmes de rendu 3D de première génération'''. De tels systèmes ont permis d'implémenter le rendu à primitives colorées qu'on a vu il y a quelques chapitres, à savoir un rendu où les triangles sont coloriés avec une couleur unique. Les systèmes de première génération étaient simples : des processeurs pour le calcul de la géométrie, un circuit de rastérisation, une RAM pour le ''framebuffer'' et des ASIC servant de ROPs très simples. Il n'y avait pas d'élimination des pixels cachés, pas de textures, et encore moins d'éclairage par pixels. Le premier système de ce genre était le ''Shaded Picture System'', toujours par Evans & Sutherland. Il ne gérait pas la couleur et ne pouvait afficher que des images en noir et blanc, mais il gérait l'éclairage par sommet (''vertex lighting''). Il a rapidement été dépassé par les systèmes de l'entreprise ''Silicon Graphics Inc'' (SGI), ainsi que ceux de l'entreprise Apollo avec sa série Apollo DN. Les '''systèmes de seconde génération''' sont apparus vers la fin des années 80, et se distinguent des précédents par l'ajout un tampon de profondeur. Ils intègrent aussi des capacités d'éclairage par pixel, à savoir de l'éclairage plat, de Gouraud, voire de Phong ! Enfin, les '''systèmes de troisième génération''' ont acquis des capacités de placage de texture, que les systèmes précédents n'avaient pas. Ils ont aussi ajouté un support de l'antialiasing. Les systèmes SGI avec placage de texture ont déjà été abordé au chapitre précédent, dans la section sur les GPU en mode immédiat et à ''tile''. Aussi, nous ne reviendrons pas dessus. [[File:Evolution de l'architecture des premières cartes graphiques, dans les années 80-90.png|centre|vignette|upright=2.5|Evolution de l'architecture des premières cartes graphiques, dans les années 80-90]] Les systèmes de première, seconde et troisième génération avaient de nombreux points communs. En premier lieu, ils étaient fabriqués en connectant plusieurs cartes électroniques : une carte pour les calculs géométriques, une ou plusieurs cartes pour le reste du rendu graphique, une carte dédiée au VDC et avec un connecteur écran. Les transistors de l'époque n'étaient pas encore miniaturisés, ce qui fait que le système graphique ne pouvait pas tenir sur une seule carte électronique. Il n'y avait donc pas de carte graphique proprement dit, mais un équivalent éclaté sur plusieurs cartes électroniques. La carte pour la géométrie contenait typiquement une mémoire FIFO pour accumuler les commandes de rendu, un processeur de commande, et plusieurs processeurs géométriques. Les processeurs géométriques étaient parfois conçus sur mesure, comme l'a été le le ''Geometry Engine'' de SGI. Mais il est arrivé qu'ils utilisent des processeurs commerciaux comme le Weitek 3222, l'Intel i860, etc. Les processeurs pouvaient être placés en série ou en parallèle, comme expliqué dans le chapitre précédent. Le circuit de rastérisation était réalisé soit avec un processeur dédié, soit avec un circuit fixe, soit un mélange des deux. La rastérisation est en effet réalisée en plusieurs étapes, certaines peuvent être implémentées avec un processeur et d'autres avec des circuits fixes. Un point important est qu'à l'époque, le rendu n'utilisait pas que des triangles, mais des polygones en général. Ce n'est que par la suite que le rendu s'est focalisé sur les triangles et les ''quads'' (quadrilatères). Il arrivait que le système graphique gérait partiellement des polygones concaves, voire convexes. Sur les systèmes SGI, les calculs géométriques se faisaient avec des polygones, que la rastérisation découpait en triangles, le reste du rendu se faisait avec des triangles. Les stations de travail Apollo DN 10000VS découpaient les polygones en trapézoïdes orientés à l'horizontale, alignés avec des ''scanlines''. D'autres systèmes découpaient tout en triangle lors de l'étape géométrique ==Les précurseurs grand public : les bornes d'arcade== [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] L'accélération du rendu 3D sur les bornes d'arcade était déjà bien avancé dès les années 90. Les bornes d'arcade ont toujours été un segment haut de gamme de l'industrie du jeu vidéo, aussi ce n'est pas étonnant. Le prix d'une borne d'arcade dépassait facilement les 10 000 dollars pour les plus chères et une bonne partie du prix était celui du matériel informatique. Le matériel était donc très puissant et débordait de mémoire RAM comparé aux consoles de jeu et aux PC. La plupart des bornes d'arcade utilisaient du matériel standardisé entre plusieurs bornes. A l'intérieur d'une borne d'arcade se trouve une '''carte de borne d'arcade''' qui est une carte mère avec un ou plusieurs processeurs, de la RAM, une carte graphique, un VDC et pas mal d'autres matériels. La carte est reliée aux périphériques de la borne : joysticks, écran, pédales, le dispositif pour insérer les pièces afin de payer, le système sonore, etc. Le jeu utilisé pour la borne est placé dans une cartouche qui est insérée dans un connecteur spécialisé. Les cartes de bornes d'arcade étaient généralement assez complexes, elles avaient une grande taille et avaient plus de composants que les cartes mères de PC. Chaque carte contenait un grand nombre de chips pour la mémoire RAM et ROM, et il n'était pas rare d'avoir plusieurs processeurs sur une même carte. Et il n'était pas rare d'avoir trois à quatre cartes superposées dans une seule borne. Pour ceux qui veulent en savoir plus, Fabien Sanglard a publié gratuitement un livre sur le fonctionnement des cartes d'arcade CPS System, disponible via ce lien : [https://fabiensanglard.net/b/cpsb.pdf The book of CP System]. Les premières cartes graphiques des bornes d'arcade étaient des cartes graphiques 2D auxquelles on avait ajouté quelques fonctionnalités. Les sprites pouvaient être tournés, agrandit/réduits, ou déformés pour simuler de la perspective et faire de la fausse 3D. Par la suite, le vrai rendu 3D est apparu sur les bornes d'arcade. Dès 1988, la carte d'arcade Namco System 21 et Sega Model 1 géraient les calculs géométriques. Quelques années plus tard, les cartes graphiques se sont mises à supporter un éclairage de Gouraud et du placage de texture. Par exemple, le Namco System 22 et la Sega model 2 supportaient des textures 2D et comme le filtrage de texture (bilinéaire et trilinéaire), le mip-mapping, et quelques autres. Au passage, les cartes graphiques de la Namco System 22 étaient développées en partenariat avec Eans & Sutherland, qui avait commencé à se diversifier dans le marché grand public. Les cartes graphiques de l'époque faisaient les calculs géométriques sur plusieurs processeurs, généralement des processeurs de type DSP (des processeurs spécialisés dans le traitement de signal). Par exemple, la Namco System 2 utilisait 4 DSP de marque Texas Instruments TMS320C25, cadencés à 24,576 MHz. La carte d'arcade Sega Model 1 utilisait quant à elle un DSP spécialisé dans les calculs géométriques. Par la suite, les bornes d'arcade ont réutilisé le hardware des PC et autres consoles de jeux. ==La 3D sur les consoles de quatrième/cinquième génération== Les consoles avant la quatrième génération de console étaient des consoles purement 2D, sans circuits d'accélération 3D. Leur carte graphique était un simple VDC 2D, plus ou moins performant selon la console. Les premières consoles de jeu capables de rendu 3D par elles-mêmes sont les consoles dites de 5ème génération. Il y a diverses manières de classer les consoles en générations, la plus commune place la 3D à la 5ème génération, mais détailler ces controverses quant à ce classement nous amènerait trop loin. Les consoles de génération avaient une architecture assez différente des systèmes antérieurs. Les systèmes SGI et assimilés pouvaient se permettre de couter assez cher, d'utiliser beaucoup de circuits, de prendre beaucoup de place. Les bornes d'arcade sont aussi dans ce cas. Aussi, il n'était pas rare que les cartes 3D de l'époque tiennent sur plusieurs cartes électroniques séparées. Mais une console ne peut pas se permettre ce genre de folies. Aussi, les cartes 3D des consoles de l'époque tenaient dans un seul circuit intégré, comme il est d'usage de nos jours. La conséquence est que certains circuits étaient fortement simplifiés, sur les consoles de cinquième génération. Et cela a impacté l'architecture interne des GPU des consoles. Les systèmes SGI avaient plusieurs processeurs pour calculer la géométrie, couplés à plusieurs unités non-programmables pour les pixels/textures. Les cartes 3D des consoles gardaient cette organisation : processeurs pour la géométrie, circuits fixes pour le reste. Mais elles se débrouillaient souvent avec un seul processeur, voire aucun ! Dans ce dernier cas, la géométrie était calculée sur le processeur principal, le CPU. Les unités pour les pixels étaient aussi moins nombreuses, mais il y en avait plusieurs, pour profiter de l'amplification des pixels. : Les cartes 3D des consoles de jeu utilisaient le placage de texture inverse, avec quelques exceptions qui utilisaient le placage de texture direct. ===Le rendu 3D sur les consoles de quatrième génération : la SNES=== Plus haut, j'ai dit que les consoles de quatrième génération n'avaient pas de carte accélératrice 3D. Pourtant, elles ont connus quelques jeux en vraie 3D. La raison à cela est que la 3D était calculée par un GPU placé dans les cartouches du jeu ! Par exemple, les cartouches de Starfox et de Super Mario 2 contenaient un coprocesseur Super FX, qui gérait des calculs de rendu 2D/3D. En tout, il y a environ 16 coprocesseurs pour la SNES et on en trouve facilement la liste sur le net. La console était conçue pour, des pins sur les ports cartouches étaient prévues pour des fonctionnalités de cartouche annexes, dont ces coprocesseurs. Ces pins connectaient le coprocesseur au bus des entrées-sorties. Les coprocesseurs des cartouches de NES avaient souvent de la mémoire rien que pour eux, qui était intégrée dans la cartouche. Ceci étant dit, passons aux consoles de cinquième génération. ===La Nintendo 64 : un GPU avancé=== La Nintendo 64 avait le GPU le plus complexe comparé aux autres consoles, et dépassait même les cartes graphiques des PC. Il faut dire que son GPU a été conçu avec l'aide de l'entreprise SGI, dont on a vu les systèmes graphiques plus haut. Le GPU de la N64 incorporait une unité pour les calculs géométriques, un circuit de rasterisation, une unité de textures et un ROP final pour les calculs de transparence/brouillard/antialiasing, ainsi qu'un circuit pour gérer la profondeur des pixels. En somme, tout le pipeline graphique était implémenté dans le GPU de la Nintendo 64, chose très en avance sur son temps, comparé au PC ou aux autres consoles ! Le GPU est construit autour d'un processeur dédié aux calculs géométriques, le ''Reality Signal Processor'' (RSP), autour duquel on a ajouté des circuits pour le reste du pipeline graphique. L'unité de calcul géométrique est un processeur MIPS R4000, un processeur assez courant à l'époque, auquel on avait retiré quelques fonctionnalités inutiles pour le rendu 3D. Il était couplé à 4 KB de mémoire vidéo, ainsi qu'à 4 KB de mémoire ROM. Le reste du GPU était réalisé avec des circuits fixes. Un point intéressant est que le programme exécuté par le RSP pouvait être programmé ! Le RSP gérait déjà des espèces de proto-shaders, qui étaient appelés des ''[https://ultra64.ca/files/documentation/online-manuals/functions_reference_manual_2.0i/ucode/microcode.html micro-codes]'' dans la documentation de l'époque. La ROM associée au RSP mémorise cinq à sept programmes différents, aux fonctionnalités différentes. * Les microcodes gspFast3D et gspF3DNoN, implémentent un rendu 3D normal, avec des options de ''clipping'' différentes entre les deux. * Le microcode gspTurbo3D fait la même chose, mais avec moins de fonctionnalités et avec une précision réduite. Il ne gère pas le ''clipping'', l'éclairage par pixel, la correction de perspective, l'antialiasing et quelques autres fonctionnalités. Il gère cependant l'éclairage de Gouraud. Il utilise une ''display list'' simplifiée comparé aux deux microcodes précédents. * Le microcode gspZ-Sort effectue une pré-passe z, à savoir qu'il calcule le tampon de profondeur final de la scène 3D, sans rendre l'image. Cela sert à faire une élimination des pixels cachés parfaite, en logiciel. On calcule le tampon de profondeur pour déterminer quels pixels sont visibles, puis une seconde passe rend l'image en, rejetant les pixels non-visibles. * Le microcode gspSprite2D implémente un rendu 2D émulé : les sprites et arrière-plan sont des rectangles texturés. Le microcode gspS2DEX fait la même chose, mais sert à émuler le rendu de la SNES plus qu'autre chose. * Le microcode gspLine3D ne gére que des lignes, pas de triangles. Il sert pour du rendu en fil de fer. Ils géraient le rendu 3D de manière différente et avec une gestion des ressources différentes. Très peu de studios de jeu vidéo ont développé leur propre microcodes N64, car la documentation était mal faite, que Nintendo ne fournissait pas de support officiel pour cela, que les outils de développement ne permettaient pas de faire cela proprement et efficacement. ===La Playstation 1=== Sur la Playstation 1 le calcul de la géométrie était réalisé par le processeur, la carte graphique gérait tout le reste. Et la carte graphique était un circuit fixe spécialisé dans la rasterisation et le placage de textures. Elle utilisait, comme la Nintendo 64, le placage de texture inverse, qui est apparu ensuite sur les cartes graphiques. ===La 3DO et la Sega Saturn=== La Sega Saturn et la 3DO étaient les deux seules consoles à utiliser le rendu direct. La géométrie était calculée sur le processeur, même si les consoles utilisaient parfois un CPU dédié au calcul de la géométrie. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. La Sega Saturn incorpore trois processeurs et deux GPU. Les deux GPUs sont nommés le VDP1 et le VDP2. Le VDP1 s'occupe des textures et des sprites, le VDP2 s'occupe uniquement de l'arrière-plan et incorpore un VDC tout ce qu'il y a de plus simple. Ils ne gèrent pas du tout la géométrie, qui est calculée par les trois processeurs. Le troisième processeur, la Saturn Control Unit, est un processeur de type DSP, à savoir un processeur spécialisé dans le traitement de signal. Il est utilisé presque exclusivement pour accélérer les calculs géométriques. Il avait sa propre mémoire RAM dédiée, 32 KB de SRAM, soit une mémoire locale très rapide. Les transferts entre cette RAM et le reste de l'ordinateur était géré par un contrôleur DMA intégré dans le DSP. En somme, il s'agit d'une sorte de processeur spécialisé dans la géométrie, une sorte d'unité géométrique programmable. Mais la géométrie n'était pas forcément calculée que sur ce DSP, mais pouvait être prise en charge par les 3 CPU. ==L'historique des cartes graphiques des PC, avant l'arrivée de Direct X et Open Gl== Sur PC, l'évolution des cartes graphiques a eu du retard par rapport aux consoles. Les PC sont en effet des machines multi-usage, pour lesquelles le jeu vidéo était un cas d'utilisation parmi tant d'autres. Et les consoles étaient la plateforme principale pour jouer à des jeux vidéo, le jeu vidéo PC étant plus marginal. Mais cela ne veut pas dire que le jeu PC n'existait pas, loin de là ! Un problème pour les jeux PC était que l'écosystème des PC était aussi fragmenté en plusieurs machines différentes : machines Apple 1 et 2, ordinateurs Commdore et Amiga, IBM PC et dérivés, etc. Aussi, programmer des jeux PC n'était pas mince affaire, car les problèmes de compatibilité étaient légion. C'est seulement quand la plateforme x86 des IBM PC s'est démocratisée que l'informatique grand public s'est standardisée, réduisant fortement les problèmes de compatibilité. Mais cela n'a pas suffit, il a aussi fallu que les API 3D naissent. Les API 3D comme Direct X et Open GL sont absolument cruciales pour garantir la compatibilité entre plusieurs ordinateurs aux cartes graphiques différentes. Aussi, l'évolution des cartes graphiques pour PC s'est faite main dans la main avec l'évolution des API 3D. Les fonctionnalités des cartes graphiques ont évolué dans le temps, en suivant les évolutions des API 3D. Du moins dans les grandes lignes, car il est arrivé plusieurs fois que des fonctionnalités naissent sur les cartes graphiques, pour que les fabricants forcent la main de Microsoft ou d'Open GL pour les intégrer de force dans les API 3D. Passons. ===L'introduction des premiers jeux 3D : Quake et les drivers miniGL=== L'API OpenGL est née de la main de SGI, encore eux ! SGI avait créé l'API Iris GL pour ses stations de travail Iris Graphics. Iris GL a ensuite été libéré et est devenu le standard Open GL. Open GL existait déjà avant l'apparition des cartes accélératrices 3D. Il y a avait donc déjà un terreau que les programmeurs graphiques pouvaient utiliser. Mais Open GL était surtout utilisé pour des applications industrielles, médicales (imagerie), graphiques ou militaires, pas pour le jeu vidéo. Mais cela changea avec la sortie du jeu Quake, d'IdSoftware, en 1996. Quake pouvait fonctionner en rendu logiciel, mais le programmeur responsable du moteur 3D (le célébre John Carmack) ajouta une version OpenGL du jeu. Il faut dire que le jeu était programmé sur une station de travail compatible avec OpenGL, même si aucune carte accélératrice de l'époque ne supportait OpenGL. C'était là un choix qui se révéla visionnaire. En théorie, le rendu par OpenGL aurait dû se faire intégralement en logiciel, sauf sur quelques rares stations de travail adaptées. Mais les premières cartes graphiques étaient déjà dans les starting blocks. La toute première carte 3D pour PC est la '''Rendition Vérité V1000''', sortie en Septembre 1995, soit quelques mois avant l'arrivée de la Nintendo 64. La Rendition Vérité V1000 contenait un processeur MIPS cadencé à 25 MHz, 4 mébioctets de RAM, une ROM pour le BIOS, et un RAMDAC, rien de plus. C'était un vrai ordinateur complètement programmable de bout en bout, sans aucun circuit fixe. Les programmeurs ne pouvaient cependant pas utiliser cette programmabilité avec des ''shaders'', mais elle permettait à Rendition d'implémenter n'importe quelle API 3D, que ce soit OpenGL, DirectX ou même sa son API propriétaire. La Rendition Vérité avait de bonnes performances pour ce qui est de la géométrie, mais pas pour le reste. Réaliser la rastérisation et le placage de texture en logiciel n'est pas efficace, pareil pour les opérations de fin de pipeline comme l'antialiasing. Le manque d'unités fixes très rapides pour la rastérisation, le placage de texture ou les opérations de fin de pipeline était clairement un gros défaut. Mais la Rendition Vérité était un cas à part, une exception dans le paysage des cartes 3D de l'époque, qui ne faisait rien comme les autres. Les autres cartes graphiques, sorties peu après, étaient les Voodoo de 3dfx, les Riva TNT de NVIDIA, les Rage/3D d'ATI, la Virge/3D de S3, et la Matrox Mystique. Elles avaient choisit le compromis inverse de la Rendition Vérité V1000 : de bonnes performances pour le placage de textures et la rastérization, mais pas pour les calculs géométriques. Pour rappel, les systèmes professionnels et les consoles avaient des processeurs pour la géométrie, et des circuits fixes pour le reste. Les cartes graphiques de PC se passaient des processeurs pour la géométrie, les calculs géométriques étaient réalisés par le CPU. Les toutes premières cartes 3D pour PC contenaient seulement des circuits pour gérer les textures et des ROPs. Elle géraient le ''z-buffer'' en mémoire vidéo, ainsi que des effets de brouillard. Il n'y avait même pas de circuit pour la rastérisation, qui était faite en logiciel, avec les calculs géométriques. [[File:Architecture de base d'une carte 3D - 2.png|centre|vignette|upright=1.5|Carte 3D sans rasterization matérielle.]] Les cartes suivantes ajoutèrent une gestion des étapes de ''rasterization'' directement en matériel. Les cartes ATI rage 2, les Invention de chez Rendition, et d'autres cartes graphiques supportaient la rasterisation en hardware. [[File:Architecture de base d'une carte 3D - 3.png|centre|vignette|upright=1.5|Carte 3D avec gestion de la géométrie.]] Pour exploiter les unités de texture et le circuit de rastérisation, OpenGL et Direct 3D étaient partiellement implémentées en logiciel, car les cartes graphiques ne supportaient pas toutes les fonctionnalités de l'API. C'était l'époque du miniGL, des implémentations partielles d'OpenGL, fournies par les fabricants de cartes 3D, implémentées dans les pilotes de périphériques de ces dernières. Les fonctionnalités d'OpenGL implémentées dans ces pilotes étaient presque toutes exécutées en matériel, par la carte graphique. Avec l'évolution du matériel, les pilotes de périphériques devinrent de plus en plus complets, au point de devenir des implémentations totales d'OpenGL. Mais au-delà d'OpenGL, chaque fabricant de carte graphique avait sa propre API propriétaire, qui était gérée par leurs pilotes de périphériques (''drivers''). Par exemple, les premières cartes graphiques de 3dfx interactive, les fameuses voodoo, disposaient de leur propre API graphique, l'API Glide. Elle facilitait la gestion de la géométrie et des textures, ce qui collait bien avec l'architecture de ces cartes 3D. Mais ces API propriétaires tombèrent rapidement en désuétude avec l'évolution de DirectX et d'OpenGL. Direct X était une API dans l'ombre d'Open GL. La première version de Direct X qui supportait la 3D était DirectX 2.0 (juin 2, 1996), suivie rapidement par DirectX 3.0 (septembre 1996). Elles dataient d'avant le jeu Quake, et elles étaient très éloignées du hardware des premières cartes graphiques. Elles utilisaient un système d'''execute buffer'' pour communiquer avec la carte graphique, Microsoft espérait que le matériel 3D implémenterait ce genre de système. Ce qui ne fu pas le cas. Direct X 4.0 a été abandonné en cours de développement pour laisser à une version 5.0 assez semblable à la 2.0/3.0. Le mode de rendu laissait de côté les ''execute buffer'' pour coller un peu plus au hardware de l'époque. Mais rien de vraiment probant comparé à Open GL. Même Windows utilisait Open GL au lieu de Direct X maison... C'est avec Direct X 6.0 que Direct X est entré dans la cours des grands. Il gérait la plupart des technologies supportées par les cartes graphiques de l'époque. ===Le ''multi-texturing'' de l'époque Direct X 6.0 : combiner plusieurs textures=== Une technologie très importante standardisée par Dirext X 6 est la technique du '''''multi-texturing'''''. Avec ce qu'on a dit dans le chapitre précédent, vous pensez sans doute qu'il n'y a qu'une seule texture par objet, qui est plaquée sur sa surface. Mais divers effet graphiques demandent d'ajouter des textures par dessus d'autres textures. En général, elles servent pour ajouter des détails, du relief, sur une surface pré-existante. Un exemple intéressant vient des jeux de tir : ajouter des impacts de balles sur les murs. Pour cela, on plaque une texture d'impact de balle sur le mur, à la position du tir. Il s'agit là d'un exemple de '''''decals''''', des petites textures ajoutées sur les murs ou le sol, afin de simuler de la poussière, des impacts de balle, des craquelures, des fissures, des trous, etc. Les textures en question sont de petite taille et se superposent à une texture existante, plus grande. Rendre des ''decals'' demande de pouvoir superposer deux textures. Direct X 6.0 supportait l'application de plusieurs textures directement dans le matériel. La carte graphique devait être capable d'accéder à deux textures en même temps, ou du moins faire semblant que. Pour cela, elle doublaient les unités de texture et adaptaient les connexions entre unités de texture et mémoire vidéo. La mémoire vidéo devait être capable de gérer plusieurs accès mémoire en même temps et devait alors avoir un débit binaire élevé. [[File:Multitexturing.png|centre|vignette|upright=2|Multitexturing]] La carte graphique devait aussi gérer de quoi combiner deux textures entre elles. Par exemple, pour revenir sur l'exemple d'une texture d'impact de balle, il faut que la texture d'impact recouvre totalement la texture du mur. Dans ce cas, la combinaison est simple : la première texture remplace l'ancienne, là où elle est appliquée. Mais les cartes graphiques ont ajouté d'autres combinaisons possibles, par exemple additionner les deux textures entre elle, faire une moyenne des texels, etc. Les opérations pour combiner les textures était le fait de circuits appelés des '''''combiners'''''. Concrètement, les ''combiners'' sont de simples unités de calcul. Les ''conbiners'' ont beaucoup évolués dans le temps, mais les premières implémentation se limitaient à quelques opérations simples : addition, multiplication, superposition, interpolation. L'opération effectuer était envoyée au ''conbiner'' sur une entrée dédiée. [[File:Multitexturing avec combiners.png|centre|vignette|upright=2|Multitexturing avec combiners]] S'il y avait eu un seul ''conbiner'', le circuit de ''multitexturing'' aurait été simplement configurable. Mais dans la réalité, les premières cartes utilisant du ''multi-texturing'' utilisaient plusieurs ''combiners'' placés les uns à la suite des autres. L'implémentation des ''combiners'' retenue par Open Gl, et par le hardware des cartes graphiques, était la suivante. Les ''combiners'' étaient placés en série, l'un à la suite de l'autre, chacun combinant le résultat de l'étage précédent avec une texture. Le premier ''combiner'' gérait l'éclairage par sommet, afin de conserver un minimum de rétrocompatibilité. [[File:Texture combiners Open GL.png|centre|vignette|upright=2|Texture combiners Open GL]] Voici les opérations supportées par les ''combiners'' d'Open GL. Ils prennent en entrée le résultat de l'étage précédent et le combinent avec une texture lue depuis l'unité de texture. {|class="wikitable" |+ Opérations supportées par les ''combiners'' d'Open GL |- ! Replace | colspan="2" | Pixel provenant de l'unité de texture |- ! Addition | colspan="2" | Additionne l'entrée au texel lu. |- ! Modulate | colspan="2" | Multiplie l'entrée avec le texel lu |- ! Mélange (''blending'') | Moyenne pondérée des deux entrées, pondérée par la composante de transparence || La couleur de transparence du texel lu et de l'entrée sont multipliées. |- ! Decals | Moyenne pondérée des deux entrées, pondérée par la composante de transparence. || La transparence du résultat est celle de l'entrée. |} Il faut noter qu'un dernier étage de ''combiners'' s'occupait d'ajouter la couleur spéculaire et les effets de brouillards. Il était à part des autres et n'était pas configurable, c'était un étage fixe, qui était toujours présent, peu importe le nombre de textures utilisé. Il était parfois appelé le '''''combiner'' final''', terme que nous réutiliserons par la suite. Mine de rien, cela a rendu les cartes graphiques partiellement programmables. Le fait qu'il y ait des opérations enchainées à la suite, opérations qu'on peut choisir librement, suffit à créer une sorte de mini-programme qui décide comment mélanger plusieurs textures. Mais il y avait une limitation de taille : le fait que les données soient transmises d'un étage à l'autre, sans détours possibles. Par exemple, le troisième étage ne pouvait avoir comme seule opérande le résultat du second étage, mais ne pouvait pas utiliser celui du premier étage. Il n'y avait pas de registres pour stocker ce qui sortait de la rastérisation, ni pour mémoriser temporairement les texels lus. ===Le ''Transform & Lighting'' matériel de Direct X 7.0=== [[File:Architecture de base d'une carte 3D - 4.png|vignette|upright=1.5|Carte 3D avec gestion de la géométrie.]] La première carte graphique pour PC capable de gérer la géométrie en hardware fût la Geforce 256, la toute première Geforce. Son unité de gestion de la géométrie n'est autre que la bien connue '''unité T&L''' (''Transform And Lighting''). Elle implémentait des algorithmes d'éclairage de la scène 3D assez simples, comme un éclairage de Gouraud, qui étaient directement câblés dans ses circuits. Mais contrairement à la Nintendo 64 et aux bornes d'arcade, elle implémentait le tout, non pas avec un processeur classique, mais avec des circuits fixes. Avec Direct X 7.0 et Open GL 1.0, l'éclairage était en théorie limité à de l'éclairage par sommet, l'éclairage par pixel n'était pas implémentable en hardware. Les cartes graphiques ont tenté d'implémenter l'éclairage par pixel, mais cela n'est pas allé au-delà du support de quelques techniques de ''bump-mapping'' très limitées. Par exemple, Direct X 6.0 implémentait une forme limitée de ''bump-mapping'', guère plus. Un autre problème est qu'il a beaucoup d'algorithmes d'éclairages différents, aux résultats visuels différents, bien au-delà des algorithmes d'éclairage plat, de Gouraud et de Phong. Et les unités de T&L étaient souvent en retard sur les algorithmes logiciels. Les programmeurs avaient le choix entre programmer les algorithmes d’éclairage qu'ils voulaient et les exécuter en logiciel, ou utiliser ceux de l'unité de T&L. Ils choisissaient souvent la première option. Par exemple, Quake 3 Arena et Unreal Tournament n'utilisaient pas les capacités d'éclairage géométrique et préféraient utiliser leurs calculs d'éclairage logiciel fait maison. Cependant, le hardware dépassait les capacités des API et avait déjà commencé à ajouter des capacités de programmation liées au ''multi-texturing''. Les cartes graphiques de l'époque, surtout chez NVIDIA, implémentaient un système de '''''register combiners''''', une forme améliorée de ''texture combiners'', qui permettait de faire une forme limitée d'éclairage par pixel, notamment du vrai ''bump-mampping'', voire du ''normal-mapping''. Mais ce n'était pas totalement supporté par les API 3D de l'époque. Les ''registers combiners'' sont des ''texture combiners'' mais dans lesquels ont aurait retiré la stricte organisation en série. Il y a toujours plusieurs étages à la suite, qui peuvent exécuter chacun une opération, mais tous les étages ont maintenant accès à toutes les textures lues et à tout ce qui sort de la rastérisation, pas seulement au résultat de l'étape précédente. Pour cela, on ajoute des registres pour mémoriser ce qui sort des unités de texture, et pour ce qui sort de la rastérisation. De plus, on ajoute des registres temporaires pour mémoriser les résultats de chaque ''combiner'', de chaque étage. Il faut cependant signaler qu'il existe un ''combiner'' final, séparé des étages qui effectuent des opérations proprement dits. Il s'agit de l'étage qui applique la couleur spéculaire et les effets de brouillards. Il ne peut être utilisé qu'à la toute fin du traitement, en tant que dernier étage, on ne peut pas mettre d'opérations après lui. Sa sortie est directement connectée aux ROPs, pas à des registres. Il faut donc faire la distinction entre les '''''combiners'' généraux''' qui effectuent une opération et mémorisent le résultat dans des registres, et le ''combiner'' final qui envoie le résultat aux ROPs. L'implémentation des ''register combiners'' utilisait un processeur spécialisés dans les traitements sur des pixels, une sorte de proto-processeur de ''shader''. Le processeur supportait des opérations assez complexes : multiplication, produit scalaire, additions. Il s'agissait d'un processeur de type VLIW, qui sera décrit dans quelques chapitres. Mais ce processeur avait des programmes très courts. Les premières cartes NVIDIA, comme les cartes TNT pouvaient exécuter deux opérations à la suite, suivie par l'application de la couleurs spéculaire et du brouillard. En somme, elles étaient limitées à un ''shader'' à deux/trois opérations, mais c'était un début. Le nombre d'opérations consécutives est rapidement passé à 8 sur la Geforce 3. ===L'arrivée des ''shaders'' avec Direct X 8.0=== [[File:Architecture de la Geforce 3.png|vignette|upright=1.5|Architecture de la Geforce 3]] Les ''register combiners'' était un premier pas vers un éclairage programmable. Paradoxalement, l'évolution suivante s'est faite non pas dans l'unité de rastérisation/texture, mais dans l'unité de traitement de la géométrie. La Geforce 3 a remplacé l'unité de T&L par un processeur capable d'exécuter des programmes. Les programmes en question complétaient l'unité de T&L, afin de pouvoir rajouter des techniques d'éclairage plus complexes. Le tout a permis aussi d'ajouter des animations, des effets de fourrures, des ombres par ''shadow volume'', des systèmes de particule évolués, et bien d'autres. À partir de la Geforce 3 de Nvidia, les cartes graphiques sont devenues capables d'exécuter des programmes appelés '''''shaders'''''. Le terme ''shader'' vient de ''shading'' : ombrage en anglais. Grace aux ''shaders'', l'éclairage est devenu programmable, il n'est plus géré par des unités d'éclairage fixes mais été laissé à la créativité des programmeurs. Les programmeurs ne sont plus vraiment limités par les algorithmes d'éclairage implémentés dans les cartes graphiques, mais peuvent implémenter les algorithmes d'éclairage qu'ils veulent et peuvent le faire exécuter directement sur la carte graphique. Les ''shaders'' sont classifiés suivant les données qu'ils manipulent : '''''pixel shader''''' pour ceux qui manipulent des pixels, '''''vertex shaders''''' pour ceux qui manipulent des sommets. Les premiers sont utilisés pour implémenter l'éclairage par pixel, les autres pour gérer tout ce qui a trait à la géométrie, pas seulement l'éclairage par sommets. Direct X 8.0 avait un standard pour les shaders, appelé ''shaders 1.0'', qui correspondait parfaitement à ce dont était capable la Geforce 3. Il standardisait les ''vertex shaders'' de la Geforce 3, mais il a aussi renommé les ''register combiners'' comme étant des ''pixel shaders'' version 1.0. Les ''register combiners'' n'ont pas évolués depuis la Geforce 256, si ce n'est que les programmes sont passés de deux opérations successives à 8, et qu'il y avait possibilité de lire 4 textures en ''multitexturing''. A l'opposé, le processeur de ''vertex shader'' de la Geforce 3 était capable d'exécuter des programmes de 128 opérations consécutives et avait 258 registres différents ! Des ''pixels shaders'' plus évolués sont arrivés avec l'ATI Radeon 8500 et ses dérivés. Elle incorporait la technologie ''SMARTSHADER'' qui remplacait les ''registers combiners'' par un processeur de ''shader'' un peu limité. Un point est que le processeur acceptait de calculer des adresses de texture dans le ''pixel shader''. Avant, les adresses des texels à lire étaient fournis par l'unité de rastérisation et basta. L'avantage est que certains effets graphiques étaient devenus possibles : du ''bump-mapping'' avancé, des textures procédurales, de l'éclairage par pixel anisotrope, du éclairage de Phong réel, etc. Avec la Radeon 8500, le ''pixel shader'' pouvait calculer des adresses, et lire les texels associés à ces adresses calculées. Les ''pixel shaders'' pouvaient lire 6 textures, faire 8 opérations sur les texels lus, puis lire 6 textures avec les adresses calculées à l'étape précédente, et refaire 8 opérations. Quelque chose de limité, donc, mais déjà plus pratique. Les ''pixel shaders'' de ce type ont été standardisé dans Direct X 8.1, sous le nom de ''pixel shaders 1.4''. Encore une fois, le hardware a forcé l'intégration dans une API 3D. ===Les ''shaders'' de Direct X 9.0 : de vrais ''pixel shaders''=== Avec Direct X 9.0, les ''shaders'' sont devenus de vrais programmes, sans les limitations des ''shaders'' précédents. Les ''pixels shaders'' sont passés à la version 2.0, idem pour les ''vertex shaders''. Concrètement, ils ont des fonctionnalités bien supérieures à celles des ''registers combiners''. Les ''shaders'' pouvaient exécuter une suite d'opérations arbitraire, dans le sens où elle n'était pas structurée avec tel type d'opération au début, suivie par un accès aux textures, etc. On pouvait mettre n'importe quelle opération dans n'importe quel ordre. De plus, les ''shaders'' ne sont plus écrit en assembleur comme c'était le cas avant. Ils sont dorénavant écrits dans un langage de haut-niveau, le HLSL pour les shaders Direct X et le GLSL pour les shaders Open Gl. Les ''shaders'' sont ensuite traduit (compilés) en instructions machines compréhensibles par la carte graphique. Au début, ces langages et la carte graphique supportaient uniquement des opérations simples. Mais au fil du temps, les spécifications de ces langages sont devenues de plus en plus riches à chaque version de Direct X ou d'Open Gl, et le matériel en a fait autant. Le matériel s'est alors adapté, en incorporant un véritable processeur pour les ''pixel shaders''. Les ''pixel shaders'' sont maintenant exécutés par un processeur de ''shader'' dédié, aux fonctionnalités bien supérieures à celles des ''registers combiners''. Le processeur de ''pixel shader'' incorpore l'unité de texture en sont sein, les deux sont fusionnés. La raison à cela sera expliqué dans la suite du chapitre. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=1.5|Carte 3D avec pixels et vertex shaders non-unifiés.]] ===L'après Direct X 9.0=== Avant Direct X 10, les processeurs de ''shaders'' ne géraient pas exactement les mêmes opérations pour les processeurs de ''vertex shader'' et de ''pixel shader''. Les processeurs de ''vertex shader'' et de ''pixel shader''étaient séparés. Depuis DirectX 10, ce n'est plus le cas : le jeu d'instructions a été unifié entre les vertex shaders et les pixels shaders, ce qui fait qu'il n'y a plus de distinction entre processeurs de vertex shaders et de pixels shaders, chaque processeur pouvant traiter indifféremment l'un ou l'autre. [[File:Architecture de base d'une carte 3D - 6.png|centre|vignette|upright=1.5|Architecture de la GeForce 6800.]] Avec Direct X 10, de nombreux autres ''shaders'' sont apparus. Les plus liés au rendu 3D sont les '''''geometry shader''''' pour ceux qui manipulent des triangles, de ''hull shaders'' et de ''domain shaders'' pour la tesselation. De plus, les cartes graphiques modernes sont capables d’exécuter des programmes informatiques qui n'ont aucun lien avec le rendu 3D, mais sont exécutés par la carte graphique comme le ferait un processeur d'ordinateur normal. De tels ''shaders'' sans lien avec le rendu 3D sont appelés des ''compute shader''. ==Les cartes graphiques d'aujourd'hui== Les circuits d'un GPU ont beaucoup évolué depuis l'introduction des ''shaders'', pour devenir de plus en plus programmables. Mais à côté des processeurs de ''shaders'', il reste quelques circuits non-programmables appelés des circuits fixes. La rastérisation, le placage de texture, l'élimination des pixels cachés et le mélange ''alpha'' sont gérés par des circuits fixes. [[File:3D-Pipeline.svg|centre|vignette|upright=3.0|Pipeline 3D : ce qui est programmable et ce qui ne l'est pas dans une carte graphique moderne.]] Mais pourquoi ne pas tout rendre programmable ? Ou au contraire, utiliser seulement des circuits fixes ? La réponse rapide est qu'il s'agit d'un compromis entre flexibilité et performance qui permet d'avoir le meilleur des deux mondes. Mais ce compromis a fortement évolué dans le temps, comme on va le voir plus bas. Rendre l'éclairage programmable permet d'implémenter facilement un grand nombre d'effets graphiques sans avoir à les implémenter en hardware. Avant les ''shaders'', les effets graphiques derniers cri n'étaient disponibles que sur les derniers modèles de carte graphique. Avec des ''vertex/pixel shaders'', ce genre de défaut est passé à la trappe. Si un nouvel algorithme de rendu graphique est inventé, il peut être utilisé dès le lendemain sur toutes les cartes graphiques modernes. De plus, implémenter beaucoup d'algorithmes d'éclairage différents avec des circuits fixes a un cout en termes de transistors, alors qu'utiliser des circuits programmable a un cout en hardware plus limité. Tout cela est à l'exact opposé de ce qu'on a avec les autres circuits, comme les circuits pour la rastérisation ou le placage de texture. Il n'y a pas 36 façons de rastériser une scène 3D et la flexibilité n'est pas un besoin important pour cette opération, alors que les performances sont cruciales. Même chose pour le placage/filtrage de textures. En conséquences, les unités de rastérisation, de texture, et les ROPs sont toutes implémentées en matériel. Faire ainsi permet de gagner en performance sans que cela ait le moindre impact pour le programmeur. Reste à expliquer dans le détail pourquoi. ===Les unités de texture sont intégrées aux processeurs de shaders=== Avec l'arrivée des processeurs de shaders, les unités de texture ont été intégrées dans les processeurs de shaders eux-mêmes. C'est la seule unité fixe qui a subit ce traitement, et il est intéressant de comprendre pourquoi. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=2|Architecture de base d'une carte 3D.]] Pour cela, il faut faire un rappel sur ce qu'il y a dans un processeur. Un processeur contient globalement quatre circuits : * une unité de calcul qui fait des calculs ; * des registres pour stocker les opérandes et résultats des calculs ; * une unité de communication avec la mémoire ; * et un séquenceur, un circuit de contrôle qui commande les autres. L'unité de communication avec la mémoire sert à lire ou écrire des données, à les transférer de la RAM vers les registres, ou l'inverse. Lire une donnée demande d'envoyer son adresse à la RAM, qui répond en envoyant la donnée lue. Elle est donc toute indiquée pour lire une texture : lire une texture n'est qu'un cas particulier de lecture de données. Les texels à lire sont à une adresse précise, la RAM répond à la lecture avec le texel demandé. Il est donc possible d'utiliser l'unité de communication avec la mémoire comme si c'était une unité de texture. Cependant, les textures ne sont pas utilisées comme telles de nos jours. Le rendu 3D moderne utilise des techniques dites de filtrage de texture, qui permettent d'améliorer la qualité du rendu des textures. Sans ce filtrage de texture, les textures appliquées naïvement donnent un résultat assez pixelisé et assez moche, pour des raisons assez techniques. Le filtrage élimine ces artefacts, en utilisant une forme d'''antialiasing'' interne aux textures, le fameux filtrage de texture. Le filtrage de texture peut être réalisé en logiciel ou en matériel. Techniquement, il est possible de le faire dans un ''shader''. Le ''shader'' calcule les adresses des texels à lire, lit les texels, et effectue ensuite le filtrage avec des opérations de calcul. Mais ce n'est pas ce qui est fait, le filtrage de texture est toujours effectué directement en matériel. La raison est que le filtrage de texture est très simple à implémenter en hardware. Le filtrage bilinéaire ou trilinéaire demande juste des circuits d'interpolation et quelques registres, ce qui est trivial. Et la seconde raison est qu'il n'y a pas 36 façons de filtrer des textures : une carte graphique peut implémenter les algorithmes principaux existants en assez peu de circuits. Pour simplifier l'implémentation, les processeurs de ''shader'' modernes disposent d'une unité d'accès mémoire séparée de l'unité de texture. L'unité d'accès mémoire normale s'occupe des accès mémoire hors-textures, alors que l'unité mémoire s'occupe de lire les textures. L'unité de texture contient de quoi faire du filtrage de texture, mais aussi faire des calculs d'adresse spécialisées, intrinsèquement liés au format des textures, qu'on détaillera dans le chapitre sur les textures. En comparaison, les unités d'accès mémoire effectuent des calculs d'adresse plus basiques. Un dernier avantage est que l'unité de texture est reliée au cache de texture, alors que l'unité d'accès mémoire est relié au cache L1/L2. ===Les ROPs peuvent être implémentés dans le ''pixel shader''=== Les ROPs effectuent plusieurs opérations basiques, mais les deux plus importantes sont la gestion du tampon de profondeur et de la transparence. Par transparence, on veut parler du mélange ''alpha''. Pour la gestion du tampon de profondeur, on veut parler du ''z-test'', qui compare la profondeur de deux pixels/fragments. Il s'agit d'opérations simples, qu'un processeur de shader peut faire sans problèmes. Par exemple, le ''z-test'' demande de faire plusieurs étapes : * calculer l'adresse du pixel dans le tampon de profondeur ; * lire le pixel dans le tampon de profondeur ; * Faire la comparaison entre profondeurs ; * Si le résultat de la comparaison est okay : ** écrire la nouvelle valeur z dans le tampon de profondeur, et écrire le nouveau pixel dedans. Le mélange ''alpha'' demande lui de : * calculer l'adresse du pixel dans le ''framebuffer'' ; * lire le pixel dans le ''framebuffer'' ; * faire des additions et multiplications pour le mélange ''alpha'' : * écrire le nouveau pixel dans le ''framebuffer''. Pour résumer il faut pouvoir faire : calcul d'adresse, lecture, écriture, addition, multiplication et comparaisons. Et toutes ces opérations sont supportées nativement par les processeurs de shaders, ce sont des instructions communes. Il est donc possible d'émuler les ROPs dans les pixels shaders. En pratique, c'est assez rare, et il y a une bonne explication à cela. Émuler les ROPs dans un ''pixel shader'' est trivial, comme on vient de le voir. Sauf que cela ne marche que si le GPU fait le rendu un pixel à la fois. Le tampon de profondeur est conçu pour traiter un pixel à la fois, idem pour le mélange ''alpha''. Mais si on ne traite pas l'image pixel par pixel, alors les deux algorithmes dysfonctionnent. Donc, tout va bien s'il n'y a qu'un seul processeur de ''pixel shader'', et que celui-ci est conçu pour ne traiter qu'un pixel à la fois, qu'une seule instance de ''shader''. Mais cela ne marche pas sur les GPU modernes, qui ont non seulement près d'une centaine de processeurs de shaders, chacun étant conçu pour traiter une centaine de fragments/pixels en même temps ! Pour donner un exemple, imaginons la situation illustrée ci-dessous. Supposons que l'on ait assez de processeurs de shaders pour traiter plusieurs triangles en même temps. Par malchance, les processeurs rendent en même temps deux triangles opaques qui se recouvrent à l'écran. Là où ils se recouvrent, les deux triangles vont générer deux fragments par pixel, et un seul sera le bon. Pas de chance, les deux fragments sont rendus en parallèle dans deux processeurs séparés. Les deux processeurs lisent la même donnée dans le tampon de profondeur et les deux fragments passent le ''z-test'', car ils n'ont aucun moyen de savoir la coordonnée z en cours de traitement dans l'autre processeur. Les deux processeurs vont alors écrire leur résultat en mémoire et c'est premier arrivé, premier servi. Le résultat n'est pas forcément celui attendu : le pixel le plus proche peut être écrit avant le plus lointain, ou inversement. [[File:Situation où faire le z-test dans les pixel shaders dysfonctionne.png|centre|vignette|upright=2|Situation où faire le z-test dans les pixel shaders dysfonctionne]] Pour obtenir un bon rendu, le GPU doit forcer le z-test à se faire fragment par fragment, du moins quand on regarde un pixel individuel. Il reste possible de traiter des pixels différents en parallèle, mais pas deux fragments d'un même pixel. En utilisant des processeurs de shaders qui travaillent en parallèle, cette contrainte est parfois brisée et le rendu donne des résultats incorrects. Le tampon de profondeur n'est pas conçu pour être parallélisé, idem pour le mélange ''alpha''. Il faut donc une sorte de point de synchronisation dans le pipeline pour éviter tout problème. Et c'est à ça que servent les ROPs. Une solution alternative serait de mémoriser, pour chaque pixel, si un ''pixel shader'' est en train de le traiter. Il suffit de mémoriser un bit par pixel pour cela, dans une table d'utilisation, concrètement une petite mémoire. Elle serait mise à jour par les processeurs de shaders, et consultée par le rastériseur. Quand le rastériseur génère un fragment, il consulte cette table, pour vérifier s'il y a conflit avec les fragments en cours de traitement. Il attend si c'est le cas, le pixel shader finira par finir de traiter le pixel au bout d'un moment. Mais l'inconvénient de cette solution est qu'elle a besoin d'une mémoire partagée par tous les processeurs de shaders, qui est difficile à concevoir sans faire des concessions en termes de performances. Une autre solution serait de mémoriser tous les pixels en cours de traitement. Quand le rastériseur génère un fragment, il mémorise les coordonnées x,y de ce fragment à l'écran, dans une '''table des pixels occupés'''. Dès qu'un pixel shader se termine, la table des pixels occupés est mise à jour. Le rastériseur consulte cette table quand il génère un fragment, afin de détecter les conflits. S'il y a conflit, le rastériseur attend que le fragment conflictuel, en cours de traitement dans le pixel shader, soit traité. L’inconvénient de la solution précédente est que la table des pixels occupés est techniquement une mémoire associative, une sorte de mémoire cache, qui est plus complexe qu'une simple RAM. Il est très difficile de créer une mémoire de ce genre qui soit capable de mémoriser plusieurs dizaines ou centaine de milliers de pixels, pour gérer une centaine de processeurs de shaders. Par contre, elle fonctionne pas trop mal pour un petit nombre de processeurs de shaders, qui fonctionnent à basse fréquence. Cela explique que les GPU pour PC ont des ROPs séparés des processeurs de ''shaders'' : ces GPU ont beaucoup trop de processeurs de shaders. Par contre, quelques cartes graphiques destinées les smartphones et autres appareils mobiles émulent les ROPs dans les ''pixel shaders''. Mais il y a une bonne raison à cela : non seulement, ils n'ont que très peu de processeurs de shader, mais ce sont aussi des GPU en rendu à tuiles. L'avantage est qu'ils rendent une tile à la fois, ce qui fait qu'il y a besoin de tester les conflits entre fragments à l'intérieur d'une tile, pas pour l'écran complet. Et cela simplifie grandement les circuits de test, notamment la table des pixels occupés, qui est bien plus petite. {{NavChapitre | book=Les cartes graphiques | prev=Les cartes graphiques : architecture de base | prevText=Les cartes graphiques : architecture de base | next=Les processeurs de shaders | nextText=Les processeurs de shaders }} {{autocat}} egf6i6lkwza4rbtsmhrd5vmgocuelfv Les cartes graphiques/Le pipeline géométrique : évolution 0 67393 763441 763207 2026-04-11T13:41:57Z Mewtow 31375 /* Les cartes accélératrices des PC grand publics : les années 90-2000 */ 763441 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. Pour rappel, le pipeline géométrique regroupe les étapes suivantes : * L'étape de '''transformation''' effectue des changements de coordonnées pour chaque sommet. Premièrement, elle place les objets au bon endroit dans la scène 3D, ce qui demande de mettre à jour les coordonnées de chaque sommet de chaque modèle. C'est la première étape de calcul : l'''étape de transformation des modèles 3D''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * La phase d''''éclairage''' (en anglais ''lighting'') attribue une couleur à chaque sommet, qui dit si le sommet est fortement éclairé ou dans l'ombre. * La phase d''''assemblage des primitives''' regroupe les sommets en triangles. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la Geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée l''''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. En plus de l'unité de T&L, il y avait deux circuits supplémentaires : un ''input assembler'' et un circuit d'assemblage des primitives. L'''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} Le pipeline géométrique de cette période était donc assez différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Mais tout cela sera le sujet du prochaine chapitre. ==L'''input assembler''== Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du cube de l'image ci-contre. Le sommet rouge du cube appartient aux 3 faces grise, jaune et bleue, et sera présent en trois exemplaires dans le tampon de sommets : un pour la face bleue, un pour la jaune, et un pour la grise. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : On pourrait remplacer les indices par des pointeurs, ce qui donnerait un cas particulier d'une structure de données connue sous le nom de vecteur de Liffe. Mais ce n'est pas très pratique et n'est pas utilisé dans le domaine du rendu 3D. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L'''input assembler'' et le tampon d'indice=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Pour faire son travail, il a besoin de l'adresse des données géométriques en mémoire, leur taille et éventuellement du type des données qu'on lui envoie (sommets codées sur 32 bits, 64, 128, etc). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. Et en général, c'est une unité d'accès mémoire un peu particulière, qui contient des circuits assez classiques pour ce genre de circuits : des circuits de calcul d'adresse, des circuits pour commander la mémoire VRAM, un contrôleur mémoire, diverses mémoires tampons, etc. Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques. [[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]] Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela. Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice. Les unités de ''index fetch'' et de ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances. Vous remarquerez que l’''input assembler'' fait surtout des calculs d'adresse, des lectures en mémoire, et des conversions de format de données. Un processeur de ''vertex shader'' peut faire la même chose, ce qui fait qu'il est possible d'émuler l'''input assembler'' avec un ''vertex shader''. La seule condition, absolument nécessaire, est que le ''vertex shader'' puisse lire des données en mémoire vidéo. Et pas seulement lire des textures, comme le permettent les techniques de ''vertex texturing'', mais de vraies lectures arbitraires, pour lire les tampons de sommet/indice. Cette possibilité est arrivée avec Direct X 10, ce qui fait que l’''input assembler'' peut être émulé par les ''vertex shaders'' à partir de cette version de Direct X. De nos jours, tous les GPUs font à leur sauce. Certains émulent l’''input assembler'' avec des ''shaders'', d'autres non. Ceux qui le font le font en modifiant les ''vertex shaders''. Le ''driver'' du GPU injecte du code dans les ''vertex shaders'', code qui émule l'''input assembler''. ===Les caches de sommets : une optimisation du tampon d'indice=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais quand des sommets sont dupliqués, ce n'est pas le cas. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Elle est appelée le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants. L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''. Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''. Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide. Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite. [[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]] Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances. [[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]] Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'assemblage de primitives=== En sortie des unités géométriques, on a des sommets éclairés et colorisés, pas des triangles. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. Un problème pour l'assemblage de primitives est que les sommets n’arrivent pas dans l'ordre. Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées. Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle. Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles. Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} sak645x8sjljkq2zxp8hwh5wiu5bf73 763442 763441 2026-04-11T13:43:47Z Mewtow 31375 /* Les cartes accélératrices des PC grand publics : les années 90-2000 */ 763442 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. Pour rappel, le pipeline géométrique regroupe les étapes suivantes : * L'étape de '''transformation''' effectue des changements de coordonnées pour chaque sommet. Premièrement, elle place les objets au bon endroit dans la scène 3D, ce qui demande de mettre à jour les coordonnées de chaque sommet de chaque modèle. C'est la première étape de calcul : l'''étape de transformation des modèles 3D''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * La phase d''''éclairage''' (en anglais ''lighting'') attribue une couleur à chaque sommet, qui dit si le sommet est fortement éclairé ou dans l'ombre. * La phase d''''assemblage des primitives''' regroupe les sommets en triangles. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la Geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée l''''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. En plus de l'unité de T&L, il y avait deux circuits supplémentaires : un ''input assembler'' et un circuit d'assemblage des primitives. L'''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} Le pipeline géométrique de cette période était donc assez différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Avec les ''primitive shaders'', l'implémentation exacte dépend de si la tesselation est activée ou non. Si la tesselation n'est pas activée, le ''vertex shader'' et le ''geométry shader'' sont fusionnés en un seul ''primitive shader''. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, sans tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="4" | |- ! DirectX 12 | colspan="4" | ''Primitive shader'' (AMD) |} Avec la tesselation activée, les ''geometry shaders'' et les ''domain shaders'' en un seul ''shader''. De même, les ''vertex shaders'' et les ''hull shaders'' sont fusionnés en un seul ''shader'', nommé l{{'}}''amplification shader''. Ainsi, le pipeline graphique est grandement simplifié, avec seulement deux ''shaders'' et un étage fixe, au lieu de quatre ''shaders'' différents. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, avec tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="7" | |- ! DirectX 12 | colspan="3" | * ''Amplification shader'' (AMD) | class="f_rouge" | Tesselation | colspan="3" | * ''Primitive shader'' (AMD) |} ==L'''input assembler''== Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du cube de l'image ci-contre. Le sommet rouge du cube appartient aux 3 faces grise, jaune et bleue, et sera présent en trois exemplaires dans le tampon de sommets : un pour la face bleue, un pour la jaune, et un pour la grise. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : On pourrait remplacer les indices par des pointeurs, ce qui donnerait un cas particulier d'une structure de données connue sous le nom de vecteur de Liffe. Mais ce n'est pas très pratique et n'est pas utilisé dans le domaine du rendu 3D. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L'''input assembler'' et le tampon d'indice=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Pour faire son travail, il a besoin de l'adresse des données géométriques en mémoire, leur taille et éventuellement du type des données qu'on lui envoie (sommets codées sur 32 bits, 64, 128, etc). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. Et en général, c'est une unité d'accès mémoire un peu particulière, qui contient des circuits assez classiques pour ce genre de circuits : des circuits de calcul d'adresse, des circuits pour commander la mémoire VRAM, un contrôleur mémoire, diverses mémoires tampons, etc. Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques. [[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]] Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela. Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice. Les unités de ''index fetch'' et de ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances. Vous remarquerez que l’''input assembler'' fait surtout des calculs d'adresse, des lectures en mémoire, et des conversions de format de données. Un processeur de ''vertex shader'' peut faire la même chose, ce qui fait qu'il est possible d'émuler l'''input assembler'' avec un ''vertex shader''. La seule condition, absolument nécessaire, est que le ''vertex shader'' puisse lire des données en mémoire vidéo. Et pas seulement lire des textures, comme le permettent les techniques de ''vertex texturing'', mais de vraies lectures arbitraires, pour lire les tampons de sommet/indice. Cette possibilité est arrivée avec Direct X 10, ce qui fait que l’''input assembler'' peut être émulé par les ''vertex shaders'' à partir de cette version de Direct X. De nos jours, tous les GPUs font à leur sauce. Certains émulent l’''input assembler'' avec des ''shaders'', d'autres non. Ceux qui le font le font en modifiant les ''vertex shaders''. Le ''driver'' du GPU injecte du code dans les ''vertex shaders'', code qui émule l'''input assembler''. ===Les caches de sommets : une optimisation du tampon d'indice=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais quand des sommets sont dupliqués, ce n'est pas le cas. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Elle est appelée le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants. L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''. Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''. Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide. Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite. [[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]] Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances. [[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]] Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'assemblage de primitives=== En sortie des unités géométriques, on a des sommets éclairés et colorisés, pas des triangles. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. Un problème pour l'assemblage de primitives est que les sommets n’arrivent pas dans l'ordre. Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées. Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle. Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles. Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} 4gc2nixq8czt59unkbdpyi88cesgpfe 763443 763442 2026-04-11T13:45:49Z Mewtow 31375 /* L'assemblage de primitives */ 763443 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. Pour rappel, le pipeline géométrique regroupe les étapes suivantes : * L'étape de '''transformation''' effectue des changements de coordonnées pour chaque sommet. Premièrement, elle place les objets au bon endroit dans la scène 3D, ce qui demande de mettre à jour les coordonnées de chaque sommet de chaque modèle. C'est la première étape de calcul : l'''étape de transformation des modèles 3D''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * La phase d''''éclairage''' (en anglais ''lighting'') attribue une couleur à chaque sommet, qui dit si le sommet est fortement éclairé ou dans l'ombre. * La phase d''''assemblage des primitives''' regroupe les sommets en triangles. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la Geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée l''''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. En plus de l'unité de T&L, il y avait deux circuits supplémentaires : un ''input assembler'' et un circuit d'assemblage des primitives. L'''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} Le pipeline géométrique de cette période était donc assez différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Avec les ''primitive shaders'', l'implémentation exacte dépend de si la tesselation est activée ou non. Si la tesselation n'est pas activée, le ''vertex shader'' et le ''geométry shader'' sont fusionnés en un seul ''primitive shader''. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, sans tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="4" | |- ! DirectX 12 | colspan="4" | ''Primitive shader'' (AMD) |} Avec la tesselation activée, les ''geometry shaders'' et les ''domain shaders'' en un seul ''shader''. De même, les ''vertex shaders'' et les ''hull shaders'' sont fusionnés en un seul ''shader'', nommé l{{'}}''amplification shader''. Ainsi, le pipeline graphique est grandement simplifié, avec seulement deux ''shaders'' et un étage fixe, au lieu de quatre ''shaders'' différents. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, avec tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="7" | |- ! DirectX 12 | colspan="3" | * ''Amplification shader'' (AMD) | class="f_rouge" | Tesselation | colspan="3" | * ''Primitive shader'' (AMD) |} ==L'''input assembler''== Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du cube de l'image ci-contre. Le sommet rouge du cube appartient aux 3 faces grise, jaune et bleue, et sera présent en trois exemplaires dans le tampon de sommets : un pour la face bleue, un pour la jaune, et un pour la grise. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : On pourrait remplacer les indices par des pointeurs, ce qui donnerait un cas particulier d'une structure de données connue sous le nom de vecteur de Liffe. Mais ce n'est pas très pratique et n'est pas utilisé dans le domaine du rendu 3D. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L'''input assembler'' et le tampon d'indice=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Pour faire son travail, il a besoin de l'adresse des données géométriques en mémoire, leur taille et éventuellement du type des données qu'on lui envoie (sommets codées sur 32 bits, 64, 128, etc). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. Et en général, c'est une unité d'accès mémoire un peu particulière, qui contient des circuits assez classiques pour ce genre de circuits : des circuits de calcul d'adresse, des circuits pour commander la mémoire VRAM, un contrôleur mémoire, diverses mémoires tampons, etc. Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques. [[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]] Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela. Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice. Les unités de ''index fetch'' et de ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances. Vous remarquerez que l’''input assembler'' fait surtout des calculs d'adresse, des lectures en mémoire, et des conversions de format de données. Un processeur de ''vertex shader'' peut faire la même chose, ce qui fait qu'il est possible d'émuler l'''input assembler'' avec un ''vertex shader''. La seule condition, absolument nécessaire, est que le ''vertex shader'' puisse lire des données en mémoire vidéo. Et pas seulement lire des textures, comme le permettent les techniques de ''vertex texturing'', mais de vraies lectures arbitraires, pour lire les tampons de sommet/indice. Cette possibilité est arrivée avec Direct X 10, ce qui fait que l’''input assembler'' peut être émulé par les ''vertex shaders'' à partir de cette version de Direct X. De nos jours, tous les GPUs font à leur sauce. Certains émulent l’''input assembler'' avec des ''shaders'', d'autres non. Ceux qui le font le font en modifiant les ''vertex shaders''. Le ''driver'' du GPU injecte du code dans les ''vertex shaders'', code qui émule l'''input assembler''. ===Les caches de sommets : une optimisation du tampon d'indice=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais quand des sommets sont dupliqués, ce n'est pas le cas. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Elle est appelée le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants. L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''. Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''. Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide. Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite. [[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]] Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances. [[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]] Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'assemblage de primitives=== En sortie des unités géométriques, on a des sommets éclairés et colorisés, pas des triangles. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. Un problème pour l'assemblage de primitives est que les sommets n’arrivent pas dans l'ordre. Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées. Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle. Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles. Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. ==L'étape de T&L et les vertex shaders== L'''input assembler'' est suivi par une étape de '''transformation-projection'''. Elle regroupe plusieurs manipulations différentes, mais qui 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 3D du point de vue de la caméra, et un autre qui corrige la perspective. Un changement de coordonnée s'effectue assez 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 petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes, et que 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 de détail. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée unité de T&L (Transform & Lighting). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'input assembler, ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. L'unité de T&L est devenue programmable dès la Geforce 3, première carte graphique à supporter les vertex shaders. Il y a peu de généralités spécifiques pour les processeurs de vertex shaders, tout a déjà été dit dans le chapitre sur les processeurs de shaders. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des vertex shaders, distincts des processeurs pour les pixel shaders. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de vertex shader d’antan. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} l75l8b87zmc214fv42lfgkootbfwn5j 763444 763443 2026-04-11T13:46:17Z Mewtow 31375 /* L'input assembler */ 763444 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. Pour rappel, le pipeline géométrique regroupe les étapes suivantes : * L'étape de '''transformation''' effectue des changements de coordonnées pour chaque sommet. Premièrement, elle place les objets au bon endroit dans la scène 3D, ce qui demande de mettre à jour les coordonnées de chaque sommet de chaque modèle. C'est la première étape de calcul : l'''étape de transformation des modèles 3D''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * La phase d''''éclairage''' (en anglais ''lighting'') attribue une couleur à chaque sommet, qui dit si le sommet est fortement éclairé ou dans l'ombre. * La phase d''''assemblage des primitives''' regroupe les sommets en triangles. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la Geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée l''''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. En plus de l'unité de T&L, il y avait deux circuits supplémentaires : un ''input assembler'' et un circuit d'assemblage des primitives. L'''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} Le pipeline géométrique de cette période était donc assez différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Avec les ''primitive shaders'', l'implémentation exacte dépend de si la tesselation est activée ou non. Si la tesselation n'est pas activée, le ''vertex shader'' et le ''geométry shader'' sont fusionnés en un seul ''primitive shader''. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, sans tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="4" | |- ! DirectX 12 | colspan="4" | ''Primitive shader'' (AMD) |} Avec la tesselation activée, les ''geometry shaders'' et les ''domain shaders'' en un seul ''shader''. De même, les ''vertex shaders'' et les ''hull shaders'' sont fusionnés en un seul ''shader'', nommé l{{'}}''amplification shader''. Ainsi, le pipeline graphique est grandement simplifié, avec seulement deux ''shaders'' et un étage fixe, au lieu de quatre ''shaders'' différents. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, avec tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="7" | |- ! DirectX 12 | colspan="3" | * ''Amplification shader'' (AMD) | class="f_rouge" | Tesselation | colspan="3" | * ''Primitive shader'' (AMD) |} ==L'''input assembler'' et l'assemblage de primitives== Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du cube de l'image ci-contre. Le sommet rouge du cube appartient aux 3 faces grise, jaune et bleue, et sera présent en trois exemplaires dans le tampon de sommets : un pour la face bleue, un pour la jaune, et un pour la grise. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : On pourrait remplacer les indices par des pointeurs, ce qui donnerait un cas particulier d'une structure de données connue sous le nom de vecteur de Liffe. Mais ce n'est pas très pratique et n'est pas utilisé dans le domaine du rendu 3D. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L'''input assembler'' et le tampon d'indice=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Pour faire son travail, il a besoin de l'adresse des données géométriques en mémoire, leur taille et éventuellement du type des données qu'on lui envoie (sommets codées sur 32 bits, 64, 128, etc). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. Et en général, c'est une unité d'accès mémoire un peu particulière, qui contient des circuits assez classiques pour ce genre de circuits : des circuits de calcul d'adresse, des circuits pour commander la mémoire VRAM, un contrôleur mémoire, diverses mémoires tampons, etc. Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques. [[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]] Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela. Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice. Les unités de ''index fetch'' et de ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances. Vous remarquerez que l’''input assembler'' fait surtout des calculs d'adresse, des lectures en mémoire, et des conversions de format de données. Un processeur de ''vertex shader'' peut faire la même chose, ce qui fait qu'il est possible d'émuler l'''input assembler'' avec un ''vertex shader''. La seule condition, absolument nécessaire, est que le ''vertex shader'' puisse lire des données en mémoire vidéo. Et pas seulement lire des textures, comme le permettent les techniques de ''vertex texturing'', mais de vraies lectures arbitraires, pour lire les tampons de sommet/indice. Cette possibilité est arrivée avec Direct X 10, ce qui fait que l’''input assembler'' peut être émulé par les ''vertex shaders'' à partir de cette version de Direct X. De nos jours, tous les GPUs font à leur sauce. Certains émulent l’''input assembler'' avec des ''shaders'', d'autres non. Ceux qui le font le font en modifiant les ''vertex shaders''. Le ''driver'' du GPU injecte du code dans les ''vertex shaders'', code qui émule l'''input assembler''. ===Les caches de sommets : une optimisation du tampon d'indice=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais quand des sommets sont dupliqués, ce n'est pas le cas. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Elle est appelée le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants. L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''. Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''. Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide. Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite. [[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]] Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances. [[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]] Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'assemblage de primitives=== En sortie des unités géométriques, on a des sommets éclairés et colorisés, pas des triangles. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. Un problème pour l'assemblage de primitives est que les sommets n’arrivent pas dans l'ordre. Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées. Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle. Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles. Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. ==L'étape de T&L et les vertex shaders== L'''input assembler'' est suivi par une étape de '''transformation-projection'''. Elle regroupe plusieurs manipulations différentes, mais qui 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 3D du point de vue de la caméra, et un autre qui corrige la perspective. Un changement de coordonnée s'effectue assez 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 petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes, et que 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 de détail. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée unité de T&L (Transform & Lighting). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'input assembler, ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. L'unité de T&L est devenue programmable dès la Geforce 3, première carte graphique à supporter les vertex shaders. Il y a peu de généralités spécifiques pour les processeurs de vertex shaders, tout a déjà été dit dans le chapitre sur les processeurs de shaders. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des vertex shaders, distincts des processeurs pour les pixel shaders. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de vertex shader d’antan. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} dodu2o1zhbqmfgckulyui6r2xmuscij 763446 763444 2026-04-11T13:51:11Z Mewtow 31375 Déplacement autre chapitre 763446 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. Pour rappel, le pipeline géométrique regroupe les étapes suivantes : * L'étape de '''transformation''' effectue des changements de coordonnées pour chaque sommet. Premièrement, elle place les objets au bon endroit dans la scène 3D, ce qui demande de mettre à jour les coordonnées de chaque sommet de chaque modèle. C'est la première étape de calcul : l'''étape de transformation des modèles 3D''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * La phase d''''éclairage''' (en anglais ''lighting'') attribue une couleur à chaque sommet, qui dit si le sommet est fortement éclairé ou dans l'ombre. * La phase d''''assemblage des primitives''' regroupe les sommets en triangles. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la Geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée l''''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. En plus de l'unité de T&L, il y avait deux circuits supplémentaires : un ''input assembler'' et un circuit d'assemblage des primitives. L'''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} Le pipeline géométrique de cette période était donc assez différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique a peu évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes. Elles ont évolué dans le temps, pour permettre des primitives plus élaborées, mais guère plus. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. : Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Avec les ''primitive shaders'', l'implémentation exacte dépend de si la tesselation est activée ou non. Si la tesselation n'est pas activée, le ''vertex shader'' et le ''geométry shader'' sont fusionnés en un seul ''primitive shader''. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, sans tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="4" | |- ! DirectX 12 | colspan="4" | ''Primitive shader'' (AMD) |} Avec la tesselation activée, les ''geometry shaders'' et les ''domain shaders'' en un seul ''shader''. De même, les ''vertex shaders'' et les ''hull shaders'' sont fusionnés en un seul ''shader'', nommé l{{'}}''amplification shader''. Ainsi, le pipeline graphique est grandement simplifié, avec seulement deux ''shaders'' et un étage fixe, au lieu de quatre ''shaders'' différents. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, avec tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="7" | |- ! DirectX 12 | colspan="3" | * ''Amplification shader'' (AMD) | class="f_rouge" | Tesselation | colspan="3" | * ''Primitive shader'' (AMD) |} ==La tessellation== La '''tessellation''' est une technique qui permet d'ajouter des triangles à une surface à la volée. Les techniques de tesselation décomposent chaque triangle en sous-triangles plus petits, et modifient les coordonnées des sommets créés lors de ce processus. L'algorithme de découpage des triangles et la modification des coordonnées varie beaucoup selon la carte graphique ou le logiciel utilisé. Typiquement, les cartes graphiques actuelles ont un algorithme matériel pour le découpage des triangles qui est juste configurable, mais la modification des coordonnées des nouveaux sommets est programmable depuis DirectX 11. [[File:Tesselation pipeline.svg|centre|vignette|upright=2.0|Tessellation.]] Elle permet d'obtenir un bon niveau de détail géométrique, sans pour autant remplir la mémoire vidéo de sommets pré-calculés. Lire des sommets depuis la mémoire vidéo est une opération couteuse, même si les caches de sommets limitent la casse. La tesselation permet de lire un nombre limité de sommets depuis la mémoire vidéo, mais ajoute des sommets supplémentaires dans les unités de gestion de la géométrie. Les détails géométriques ajoutés par la tesselation demandent donc de la puissance de calcul, mais réduisent les accès mémoire. ===L'historique de la tesselation sur les cartes 3D=== Les premières tentatives utilisaient des algorithmes matériels de tesselation, et non des ''shaders''. Par exemple, la première carte graphique commerciale avec de la tesselation matérielle était la Radeon 8500, de l'entreprise ATI (aujourd'hui rachetée par AMD), avec la technologie de tesselation TrueForm. Elle utilisait un circuit non-programmable, qui tessellait certaines surfaces et interpolait la forme de la surface entre les sommets. ATI améliora ensuite le TrueForm pour que des informations de tesselation soient lues depuis une texture, ce qui permet une implémentation de la technologie dite du ''displacement mapping''. En même temps, Matrox ajouta un algorithme de tesselation basé sur la technique de N-patch dans ses cartes graphiques. Mais ces techniques se basaient sur des algorithmes matériels non-programmables, ce qui rendait ces technologies insuffisamment flexibles et impraticables. De plus, c'était des technologies propriétaires, que les autres fabricants de cartes graphiques n'ont pas adopté. Elles sont donc tombées en désuétude. La tesselation a eu un regain d'intérêt à l'arrivée des '''''geometry shaders''''' dans DirectX 10 et OpenGL 3.2. Et il y avait de quoi, de tels shaders pouvant en théorie implémenter une forme limitée de tesselation. Mais le cout en performance est trop important, sans compter que les limitations de ces shaders n'a pas permis leur usage pour de la tesselation généraliste. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | DirectX 9 |- | class="f_rouge" | ''Input assembly'' | colspan="2" | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="4" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Une nouvelle étape a été franchie avec l'AMD Radeon HD2000 et le GPU de la Xbox 360, qui permettaient une tesselation partiellement programmable. La tesselation se faisait en deux étapes : une étape de découpage des triangles et une étape de modification des sommets créés. La première étape était un algorithme matériel configurable mais non-programmable, alors que la seconde était programmable. Mais le manque de support logiciel, le fait qu'on ne pouvait pas utiliser la tesselation en même temps que les ''geometry shader'', ainsi que la non-utilisation de cette technique par NVIDIA, a fait que cette technique n'a pas été reprise dans les GPU suivants. Il fallut attendre l'arrivée des '''tesselation shaders''' dans OpenGL 4.0 et DirectX 11 pour que des shaders adéquats arrivent sur le marché commercial. La tesselation sur ces cartes graphiques se fait en trois étapes : deux ''shaders'' et un algorithme matériel fixe entre les deux. Dans le détail, un ''hull shader'' est suivi par un étage fixe de tesselation, lui-même suivi par un ''domain shader''. L'étage fixe est là où se situe le découpage des triangles par l'unité matérielle configurable. La tesselation est suivie par la modification de la place des vertices créées, mais il y a aussi un shader avant la génération des nouveaux triangles. {|class="wikitable" |- ! colspan="7" | Avant DirectX 11 |- | class="f_rouge" | ''Input assembly'' | colspan="4" | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Les ''geometry shaders'' et les ''tesselation shaders'' étaient très limités, ce qui fait qu'ils ont été peu utilisés. Les programmeurs avaient beaucoup de mal à les utiliser de manière performante, sans compter que ces ''shaders'' s'intégraient très mal au pipeline graphique existant. Les cartes graphiques avaient du mal à les intégrer au hardware, sauf à recourir à des méthodes quelque peu tordues, comme on le verra dans ce qui suit. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} qxp18w1n8o5vxe14bv3p1zjqxdcozre 763447 763446 2026-04-11T13:54:15Z Mewtow 31375 763447 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. Pour rappel, le pipeline géométrique regroupe les étapes suivantes : * L'étape de '''transformation''' effectue des changements de coordonnées pour chaque sommet. Premièrement, elle place les objets au bon endroit dans la scène 3D, ce qui demande de mettre à jour les coordonnées de chaque sommet de chaque modèle. C'est la première étape de calcul : l'''étape de transformation des modèles 3D''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * La phase d''''éclairage''' (en anglais ''lighting'') attribue une couleur à chaque sommet, qui dit si le sommet est fortement éclairé ou dans l'ombre. * La phase d''''assemblage des primitives''' regroupe les sommets en triangles. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la Geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée l''''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. En plus de l'unité de T&L, il y avait deux circuits supplémentaires : un ''input assembler'' et un circuit d'assemblage des primitives. L'''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} Les autres cartes graphiques de l'époque avaient une implémentation similaire, sur PC et console. Soit elles ne faisaient pas de calculs géométriques, soit elles avaient une unité de T&L. Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. Rapidement, dès la Geforce 3, les circuits de T&L ont été remplacés par des processeurs de ''shaders'' programmables, avec l'introduction des ''vertex shaders''. Ils s'occupaient donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. Il y a peu de généralités spécifiques pour les processeurs de ''vertex shaders'', tout a déjà été dit dans le chapitre sur les processeurs de ''shaders''. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des ''vertex shaders'', distincts des processeurs pour les ''pixel shaders''. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de ''vertex shader'' d’antan. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Avec les ''primitive shaders'', l'implémentation exacte dépend de si la tesselation est activée ou non. Si la tesselation n'est pas activée, le ''vertex shader'' et le ''geométry shader'' sont fusionnés en un seul ''primitive shader''. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, sans tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="4" | |- ! DirectX 12 | colspan="4" | ''Primitive shader'' (AMD) |} Avec la tesselation activée, les ''geometry shaders'' et les ''domain shaders'' en un seul ''shader''. De même, les ''vertex shaders'' et les ''hull shaders'' sont fusionnés en un seul ''shader'', nommé l{{'}}''amplification shader''. Ainsi, le pipeline graphique est grandement simplifié, avec seulement deux ''shaders'' et un étage fixe, au lieu de quatre ''shaders'' différents. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, avec tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="7" | |- ! DirectX 12 | colspan="3" | * ''Amplification shader'' (AMD) | class="f_rouge" | Tesselation | colspan="3" | * ''Primitive shader'' (AMD) |} ==La tessellation== La '''tessellation''' est une technique qui permet d'ajouter des triangles à une surface à la volée. Les techniques de tesselation décomposent chaque triangle en sous-triangles plus petits, et modifient les coordonnées des sommets créés lors de ce processus. L'algorithme de découpage des triangles et la modification des coordonnées varie beaucoup selon la carte graphique ou le logiciel utilisé. Typiquement, les cartes graphiques actuelles ont un algorithme matériel pour le découpage des triangles qui est juste configurable, mais la modification des coordonnées des nouveaux sommets est programmable depuis DirectX 11. [[File:Tesselation pipeline.svg|centre|vignette|upright=2.0|Tessellation.]] Elle permet d'obtenir un bon niveau de détail géométrique, sans pour autant remplir la mémoire vidéo de sommets pré-calculés. Lire des sommets depuis la mémoire vidéo est une opération couteuse, même si les caches de sommets limitent la casse. La tesselation permet de lire un nombre limité de sommets depuis la mémoire vidéo, mais ajoute des sommets supplémentaires dans les unités de gestion de la géométrie. Les détails géométriques ajoutés par la tesselation demandent donc de la puissance de calcul, mais réduisent les accès mémoire. ===L'historique de la tesselation sur les cartes 3D=== Les premières tentatives utilisaient des algorithmes matériels de tesselation, et non des ''shaders''. Par exemple, la première carte graphique commerciale avec de la tesselation matérielle était la Radeon 8500, de l'entreprise ATI (aujourd'hui rachetée par AMD), avec la technologie de tesselation TrueForm. Elle utilisait un circuit non-programmable, qui tessellait certaines surfaces et interpolait la forme de la surface entre les sommets. ATI améliora ensuite le TrueForm pour que des informations de tesselation soient lues depuis une texture, ce qui permet une implémentation de la technologie dite du ''displacement mapping''. En même temps, Matrox ajouta un algorithme de tesselation basé sur la technique de N-patch dans ses cartes graphiques. Mais ces techniques se basaient sur des algorithmes matériels non-programmables, ce qui rendait ces technologies insuffisamment flexibles et impraticables. De plus, c'était des technologies propriétaires, que les autres fabricants de cartes graphiques n'ont pas adopté. Elles sont donc tombées en désuétude. La tesselation a eu un regain d'intérêt à l'arrivée des '''''geometry shaders''''' dans DirectX 10 et OpenGL 3.2. Et il y avait de quoi, de tels shaders pouvant en théorie implémenter une forme limitée de tesselation. Mais le cout en performance est trop important, sans compter que les limitations de ces shaders n'a pas permis leur usage pour de la tesselation généraliste. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | DirectX 9 |- | class="f_rouge" | ''Input assembly'' | colspan="2" | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="4" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Une nouvelle étape a été franchie avec l'AMD Radeon HD2000 et le GPU de la Xbox 360, qui permettaient une tesselation partiellement programmable. La tesselation se faisait en deux étapes : une étape de découpage des triangles et une étape de modification des sommets créés. La première étape était un algorithme matériel configurable mais non-programmable, alors que la seconde était programmable. Mais le manque de support logiciel, le fait qu'on ne pouvait pas utiliser la tesselation en même temps que les ''geometry shader'', ainsi que la non-utilisation de cette technique par NVIDIA, a fait que cette technique n'a pas été reprise dans les GPU suivants. Il fallut attendre l'arrivée des '''tesselation shaders''' dans OpenGL 4.0 et DirectX 11 pour que des shaders adéquats arrivent sur le marché commercial. La tesselation sur ces cartes graphiques se fait en trois étapes : deux ''shaders'' et un algorithme matériel fixe entre les deux. Dans le détail, un ''hull shader'' est suivi par un étage fixe de tesselation, lui-même suivi par un ''domain shader''. L'étage fixe est là où se situe le découpage des triangles par l'unité matérielle configurable. La tesselation est suivie par la modification de la place des vertices créées, mais il y a aussi un shader avant la génération des nouveaux triangles. {|class="wikitable" |- ! colspan="7" | Avant DirectX 11 |- | class="f_rouge" | ''Input assembly'' | colspan="4" | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Les ''geometry shaders'' et les ''tesselation shaders'' étaient très limités, ce qui fait qu'ils ont été peu utilisés. Les programmeurs avaient beaucoup de mal à les utiliser de manière performante, sans compter que ces ''shaders'' s'intégraient très mal au pipeline graphique existant. Les cartes graphiques avaient du mal à les intégrer au hardware, sauf à recourir à des méthodes quelque peu tordues, comme on le verra dans ce qui suit. Pour résumer, le pipeline géométrique des PC est donc assez différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique a beaucoup évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes, pour quasiment disparaitre sur les GPU modernes. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Et de nombreuses étapes ont été rajoutées. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} sjqm0l7m2c1dj7i883z66c232t9fa3f 763448 763447 2026-04-11T13:56:18Z Mewtow 31375 /* Les cartes accélératrices des PC grand publics : les années 90-2000 */ 763448 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. Pour rappel, le pipeline géométrique regroupe les étapes suivantes : * L'étape de '''transformation''' effectue des changements de coordonnées pour chaque sommet. Premièrement, elle place les objets au bon endroit dans la scène 3D, ce qui demande de mettre à jour les coordonnées de chaque sommet de chaque modèle. C'est la première étape de calcul : l'''étape de transformation des modèles 3D''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * La phase d''''éclairage''' (en anglais ''lighting'') attribue une couleur à chaque sommet, qui dit si le sommet est fortement éclairé ou dans l'ombre. * La phase d''''assemblage des primitives''' regroupe les sommets en triangles. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la Geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée l''''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. En plus de l'unité de T&L, il y avait deux circuits supplémentaires : un ''input assembler'' et un circuit d'assemblage des primitives. L'''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} Les autres cartes graphiques de l'époque avaient une implémentation similaire, sur PC et console. Soit elles ne faisaient pas de calculs géométriques, soit elles avaient une unité de T&L. Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. La Geforce 3 a remplacé l'unité de T1L par un processeur de ''vertex shader'' programmable. Il s'occupait donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les autres GPU de l'époque ont suivi le mouvement. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. L'introduction des techniques de tesselation, qui agissent directement sur des primitives, a changé la donne. Nous détaillerons ces techniques de tesselation dans le prochain chapitre. Mais leur introduction ne s'est pas faite sans heurts. Initialement, les concepteurs de cartes graphiques ont tenté d'ajouter des nouveaux ''shaders'' pour gérer la tesselation. Sont d'abord apparus les ''geometry shaders'', puis le ''hull/domain shaders''. Il s'agit là de la troisième période, qui visait à améliorer le pipeline graphique précédent. Mais le résultat était assez désastreux. Les programmeurs avaient beaucoup de mal à utiliser la tesselation ou les nouveaux ''shaders'', ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Avec les ''primitive shaders'', l'implémentation exacte dépend de si la tesselation est activée ou non. Si la tesselation n'est pas activée, le ''vertex shader'' et le ''geométry shader'' sont fusionnés en un seul ''primitive shader''. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, sans tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="4" | |- ! DirectX 12 | colspan="4" | ''Primitive shader'' (AMD) |} Avec la tesselation activée, les ''geometry shaders'' et les ''domain shaders'' en un seul ''shader''. De même, les ''vertex shaders'' et les ''hull shaders'' sont fusionnés en un seul ''shader'', nommé l{{'}}''amplification shader''. Ainsi, le pipeline graphique est grandement simplifié, avec seulement deux ''shaders'' et un étage fixe, au lieu de quatre ''shaders'' différents. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, avec tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="7" | |- ! DirectX 12 | colspan="3" | * ''Amplification shader'' (AMD) | class="f_rouge" | Tesselation | colspan="3" | * ''Primitive shader'' (AMD) |} ==La tessellation== La '''tessellation''' est une technique qui permet d'ajouter des triangles à une surface à la volée. Les techniques de tesselation décomposent chaque triangle en sous-triangles plus petits, et modifient les coordonnées des sommets créés lors de ce processus. L'algorithme de découpage des triangles et la modification des coordonnées varie beaucoup selon la carte graphique ou le logiciel utilisé. Typiquement, les cartes graphiques actuelles ont un algorithme matériel pour le découpage des triangles qui est juste configurable, mais la modification des coordonnées des nouveaux sommets est programmable depuis DirectX 11. [[File:Tesselation pipeline.svg|centre|vignette|upright=2.0|Tessellation.]] Elle permet d'obtenir un bon niveau de détail géométrique, sans pour autant remplir la mémoire vidéo de sommets pré-calculés. Lire des sommets depuis la mémoire vidéo est une opération couteuse, même si les caches de sommets limitent la casse. La tesselation permet de lire un nombre limité de sommets depuis la mémoire vidéo, mais ajoute des sommets supplémentaires dans les unités de gestion de la géométrie. Les détails géométriques ajoutés par la tesselation demandent donc de la puissance de calcul, mais réduisent les accès mémoire. ===L'historique de la tesselation sur les cartes 3D=== Les premières tentatives utilisaient des algorithmes matériels de tesselation, et non des ''shaders''. Par exemple, la première carte graphique commerciale avec de la tesselation matérielle était la Radeon 8500, de l'entreprise ATI (aujourd'hui rachetée par AMD), avec la technologie de tesselation TrueForm. Elle utilisait un circuit non-programmable, qui tessellait certaines surfaces et interpolait la forme de la surface entre les sommets. ATI améliora ensuite le TrueForm pour que des informations de tesselation soient lues depuis une texture, ce qui permet une implémentation de la technologie dite du ''displacement mapping''. En même temps, Matrox ajouta un algorithme de tesselation basé sur la technique de N-patch dans ses cartes graphiques. Mais ces techniques se basaient sur des algorithmes matériels non-programmables, ce qui rendait ces technologies insuffisamment flexibles et impraticables. De plus, c'était des technologies propriétaires, que les autres fabricants de cartes graphiques n'ont pas adopté. Elles sont donc tombées en désuétude. La tesselation a eu un regain d'intérêt à l'arrivée des '''''geometry shaders''''' dans DirectX 10 et OpenGL 3.2. Et il y avait de quoi, de tels shaders pouvant en théorie implémenter une forme limitée de tesselation. Mais le cout en performance est trop important, sans compter que les limitations de ces shaders n'a pas permis leur usage pour de la tesselation généraliste. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | DirectX 9 |- | class="f_rouge" | ''Input assembly'' | colspan="2" | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="4" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Une nouvelle étape a été franchie avec l'AMD Radeon HD2000 et le GPU de la Xbox 360, qui permettaient une tesselation partiellement programmable. La tesselation se faisait en deux étapes : une étape de découpage des triangles et une étape de modification des sommets créés. La première étape était un algorithme matériel configurable mais non-programmable, alors que la seconde était programmable. Mais le manque de support logiciel, le fait qu'on ne pouvait pas utiliser la tesselation en même temps que les ''geometry shader'', ainsi que la non-utilisation de cette technique par NVIDIA, a fait que cette technique n'a pas été reprise dans les GPU suivants. Il fallut attendre l'arrivée des '''tesselation shaders''' dans OpenGL 4.0 et DirectX 11 pour que des shaders adéquats arrivent sur le marché commercial. La tesselation sur ces cartes graphiques se fait en trois étapes : deux ''shaders'' et un algorithme matériel fixe entre les deux. Dans le détail, un ''hull shader'' est suivi par un étage fixe de tesselation, lui-même suivi par un ''domain shader''. L'étage fixe est là où se situe le découpage des triangles par l'unité matérielle configurable. La tesselation est suivie par la modification de la place des vertices créées, mais il y a aussi un shader avant la génération des nouveaux triangles. {|class="wikitable" |- ! colspan="7" | Avant DirectX 11 |- | class="f_rouge" | ''Input assembly'' | colspan="4" | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Les ''geometry shaders'' et les ''tesselation shaders'' étaient très limités, ce qui fait qu'ils ont été peu utilisés. Les programmeurs avaient beaucoup de mal à les utiliser de manière performante, sans compter que ces ''shaders'' s'intégraient très mal au pipeline graphique existant. Les cartes graphiques avaient du mal à les intégrer au hardware, sauf à recourir à des méthodes quelque peu tordues, comme on le verra dans ce qui suit. Pour résumer, le pipeline géométrique des PC est donc assez différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique a beaucoup évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes, pour quasiment disparaitre sur les GPU modernes. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Et de nombreuses étapes ont été rajoutées. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} f598a8ioy4gzqglgg5lj7db00465o2f 763449 763448 2026-04-11T14:00:23Z Mewtow 31375 /* Les cartes accélératrices des PC grand publics : les années 90-2000 */ 763449 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. Pour rappel, le pipeline géométrique regroupe les étapes suivantes : * L'étape de '''transformation''' effectue des changements de coordonnées pour chaque sommet. Premièrement, elle place les objets au bon endroit dans la scène 3D, ce qui demande de mettre à jour les coordonnées de chaque sommet de chaque modèle. C'est la première étape de calcul : l'''étape de transformation des modèles 3D''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * La phase d''''éclairage''' (en anglais ''lighting'') attribue une couleur à chaque sommet, qui dit si le sommet est fortement éclairé ou dans l'ombre. * La phase d''''assemblage des primitives''' regroupe les sommets en triangles. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la Geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée l''''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. En plus de l'unité de T&L, il y avait deux circuits supplémentaires : un ''input assembler'' et un circuit d'assemblage des primitives. L'''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} Les autres cartes graphiques de l'époque avaient une implémentation similaire, sur PC et console. Soit elles ne faisaient pas de calculs géométriques, soit elles avaient une unité de T&L. Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. La Geforce 3 a remplacé l'unité de T&L par un processeur de ''vertex shader'' programmable. Il s'occupait donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les autres GPU de l'époque ont suivi le mouvement. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. Mais DirectX 10 a changé la donne, avec l'introduction des ''geometry shader''. Nous détaillerons ces shaders dans le prochain chapitre. Tout ce que nous dirons est que ces shaders ne travaillent pas sur des sommets isolés, mais travaillent sur des primitives, des triangles. Ils peuvent ajouter, retirer ou modifier des triangles. Mais le résultat était assez désastreux. Les ''geometry shaders'' étaient trop limitées et les programmeurs avaient beaucoup de mal à les utiliser, ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Par la suite, des shaders dédiés aux techniques de tesselations ont été introduits. Nous verrons cela dans quelques chapitres, mais les techniques de tesselations visent à augmenter les détails géométriques d'un modèle 3D, en ajoutant des sommets à la volée. Des shaders dédiés ajoutent des sommets à un modèle, en suivant un certain algorithme. {|class="wikitable" |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Depuis les années 2018-2020, le pipeline géométrique a fortement évolué et a été revu de fond en comble. L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. De nombreuses étapes ont disparues ou sont devenus programmables. A vrai dire, les étapes d'assemblage de primitives et d'''input assembly'' ont disparues. Avec les ''primitive shaders'', l'implémentation exacte dépend de si la tesselation est activée ou non. Si la tesselation n'est pas activée, le ''vertex shader'' et le ''geométry shader'' sont fusionnés en un seul ''primitive shader''. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, sans tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="4" | |- ! DirectX 12 | colspan="4" | ''Primitive shader'' (AMD) |} Avec la tesselation activée, les ''geometry shaders'' et les ''domain shaders'' en un seul ''shader''. De même, les ''vertex shaders'' et les ''hull shaders'' sont fusionnés en un seul ''shader'', nommé l{{'}}''amplification shader''. Ainsi, le pipeline graphique est grandement simplifié, avec seulement deux ''shaders'' et un étage fixe, au lieu de quatre ''shaders'' différents. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, avec tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="7" | |- ! DirectX 12 | colspan="3" | * ''Amplification shader'' (AMD) | class="f_rouge" | Tesselation | colspan="3" | * ''Primitive shader'' (AMD) |} ==La tessellation== La '''tessellation''' est une technique qui permet d'ajouter des triangles à une surface à la volée. Les techniques de tesselation décomposent chaque triangle en sous-triangles plus petits, et modifient les coordonnées des sommets créés lors de ce processus. L'algorithme de découpage des triangles et la modification des coordonnées varie beaucoup selon la carte graphique ou le logiciel utilisé. Typiquement, les cartes graphiques actuelles ont un algorithme matériel pour le découpage des triangles qui est juste configurable, mais la modification des coordonnées des nouveaux sommets est programmable depuis DirectX 11. [[File:Tesselation pipeline.svg|centre|vignette|upright=2.0|Tessellation.]] Elle permet d'obtenir un bon niveau de détail géométrique, sans pour autant remplir la mémoire vidéo de sommets pré-calculés. Lire des sommets depuis la mémoire vidéo est une opération couteuse, même si les caches de sommets limitent la casse. La tesselation permet de lire un nombre limité de sommets depuis la mémoire vidéo, mais ajoute des sommets supplémentaires dans les unités de gestion de la géométrie. Les détails géométriques ajoutés par la tesselation demandent donc de la puissance de calcul, mais réduisent les accès mémoire. ===L'historique de la tesselation sur les cartes 3D=== Les premières tentatives utilisaient des algorithmes matériels de tesselation, et non des ''shaders''. Par exemple, la première carte graphique commerciale avec de la tesselation matérielle était la Radeon 8500, de l'entreprise ATI (aujourd'hui rachetée par AMD), avec la technologie de tesselation TrueForm. Elle utilisait un circuit non-programmable, qui tessellait certaines surfaces et interpolait la forme de la surface entre les sommets. ATI améliora ensuite le TrueForm pour que des informations de tesselation soient lues depuis une texture, ce qui permet une implémentation de la technologie dite du ''displacement mapping''. En même temps, Matrox ajouta un algorithme de tesselation basé sur la technique de N-patch dans ses cartes graphiques. Mais ces techniques se basaient sur des algorithmes matériels non-programmables, ce qui rendait ces technologies insuffisamment flexibles et impraticables. De plus, c'était des technologies propriétaires, que les autres fabricants de cartes graphiques n'ont pas adopté. Elles sont donc tombées en désuétude. La tesselation a eu un regain d'intérêt à l'arrivée des '''''geometry shaders''''' dans DirectX 10 et OpenGL 3.2. Et il y avait de quoi, de tels shaders pouvant en théorie implémenter une forme limitée de tesselation. Mais le cout en performance est trop important, sans compter que les limitations de ces shaders n'a pas permis leur usage pour de la tesselation généraliste. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | DirectX 9 |- | class="f_rouge" | ''Input assembly'' | colspan="2" | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="4" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Une nouvelle étape a été franchie avec l'AMD Radeon HD2000 et le GPU de la Xbox 360, qui permettaient une tesselation partiellement programmable. La tesselation se faisait en deux étapes : une étape de découpage des triangles et une étape de modification des sommets créés. La première étape était un algorithme matériel configurable mais non-programmable, alors que la seconde était programmable. Mais le manque de support logiciel, le fait qu'on ne pouvait pas utiliser la tesselation en même temps que les ''geometry shader'', ainsi que la non-utilisation de cette technique par NVIDIA, a fait que cette technique n'a pas été reprise dans les GPU suivants. Il fallut attendre l'arrivée des '''tesselation shaders''' dans OpenGL 4.0 et DirectX 11 pour que des shaders adéquats arrivent sur le marché commercial. La tesselation sur ces cartes graphiques se fait en trois étapes : deux ''shaders'' et un algorithme matériel fixe entre les deux. Dans le détail, un ''hull shader'' est suivi par un étage fixe de tesselation, lui-même suivi par un ''domain shader''. L'étage fixe est là où se situe le découpage des triangles par l'unité matérielle configurable. La tesselation est suivie par la modification de la place des vertices créées, mais il y a aussi un shader avant la génération des nouveaux triangles. {|class="wikitable" |- ! colspan="7" | Avant DirectX 11 |- | class="f_rouge" | ''Input assembly'' | colspan="4" | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Les ''geometry shaders'' et les ''tesselation shaders'' étaient très limités, ce qui fait qu'ils ont été peu utilisés. Les programmeurs avaient beaucoup de mal à les utiliser de manière performante, sans compter que ces ''shaders'' s'intégraient très mal au pipeline graphique existant. Les cartes graphiques avaient du mal à les intégrer au hardware, sauf à recourir à des méthodes quelque peu tordues, comme on le verra dans ce qui suit. Pour résumer, le pipeline géométrique des PC est donc assez différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique a beaucoup évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes, pour quasiment disparaitre sur les GPU modernes. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Et de nombreuses étapes ont été rajoutées. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} j9ly6qta7rltcijg1x0s6wbc8rc76in 763450 763449 2026-04-11T14:02:32Z Mewtow 31375 /* Les cartes accélératrices des PC grand publics : les années 90-2000 */ 763450 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. Pour rappel, le pipeline géométrique regroupe les étapes suivantes : * L'étape de '''transformation''' effectue des changements de coordonnées pour chaque sommet. Premièrement, elle place les objets au bon endroit dans la scène 3D, ce qui demande de mettre à jour les coordonnées de chaque sommet de chaque modèle. C'est la première étape de calcul : l'''étape de transformation des modèles 3D''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * La phase d''''éclairage''' (en anglais ''lighting'') attribue une couleur à chaque sommet, qui dit si le sommet est fortement éclairé ou dans l'ombre. * La phase d''''assemblage des primitives''' regroupe les sommets en triangles. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la Geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée l''''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. En plus de l'unité de T&L, il y avait deux circuits supplémentaires : un ''input assembler'' et un circuit d'assemblage des primitives. L'''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} Les autres cartes graphiques de l'époque avaient une implémentation similaire, sur PC et console. Soit elles ne faisaient pas de calculs géométriques, soit elles avaient une unité de T&L. Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. La Geforce 3 a remplacé l'unité de T&L par un processeur de ''vertex shader'' programmable. Il s'occupait donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les autres GPU de l'époque ont suivi le mouvement. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. Mais DirectX 10 a changé la donne, avec l'introduction des ''geometry shader''. Nous détaillerons ces shaders dans le prochain chapitre. Tout ce que nous dirons est que ces shaders ne travaillent pas sur des sommets isolés, mais travaillent sur des primitives, des triangles. Ils peuvent ajouter, retirer ou modifier des triangles. Mais le résultat était assez désastreux. Les ''geometry shaders'' étaient trop limitées et les programmeurs avaient beaucoup de mal à les utiliser, ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Par la suite, des shaders dédiés aux techniques de tesselations ont été introduits. Nous verrons cela dans quelques chapitres, mais les techniques de tesselations visent à augmenter les détails géométriques d'un modèle 3D, en ajoutant des sommets à la volée. Des shaders dédiés ajoutent des sommets à un modèle, en suivant un certain algorithme. {|class="wikitable" |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. Ces shaders sont des ''geometry shaders'' améliorés, à savoir qu'ils peuvent travailler sur des primitives directement, voire sur des paquets de primitives. Concrètement, ils reçoivent des paquets de 32 sommets et exécutent des calculs dessus. Les calculs ne traitent pas chaque sommet isolément, ce qui permet de travailler sur des plusieurs triangles à la fois. Avec de tels shaders, les étapes d'assemblage de primitives et d'''input assembly'' sont inutiles, et ont disparues. Avec les ''primitive shaders'', l'implémentation exacte dépend de si la tesselation est activée ou non. Si la tesselation n'est pas activée, le ''vertex shader'' et le ''geométry shader'' sont fusionnés en un seul ''primitive shader''. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, sans tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="4" | |- ! DirectX 12 | colspan="4" | ''Primitive shader'' (AMD) |} Avec la tesselation activée, les ''geometry shaders'' et les ''domain shaders'' en un seul ''shader''. De même, les ''vertex shaders'' et les ''hull shaders'' sont fusionnés en un seul ''shader'', nommé l{{'}}''amplification shader''. Ainsi, le pipeline graphique est grandement simplifié, avec seulement deux ''shaders'' et un étage fixe, au lieu de quatre ''shaders'' différents. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, avec tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="7" | |- ! DirectX 12 | colspan="3" | * ''Amplification shader'' (AMD) | class="f_rouge" | Tesselation | colspan="3" | * ''Primitive shader'' (AMD) |} ==La tessellation== La '''tessellation''' est une technique qui permet d'ajouter des triangles à une surface à la volée. Les techniques de tesselation décomposent chaque triangle en sous-triangles plus petits, et modifient les coordonnées des sommets créés lors de ce processus. L'algorithme de découpage des triangles et la modification des coordonnées varie beaucoup selon la carte graphique ou le logiciel utilisé. Typiquement, les cartes graphiques actuelles ont un algorithme matériel pour le découpage des triangles qui est juste configurable, mais la modification des coordonnées des nouveaux sommets est programmable depuis DirectX 11. [[File:Tesselation pipeline.svg|centre|vignette|upright=2.0|Tessellation.]] Elle permet d'obtenir un bon niveau de détail géométrique, sans pour autant remplir la mémoire vidéo de sommets pré-calculés. Lire des sommets depuis la mémoire vidéo est une opération couteuse, même si les caches de sommets limitent la casse. La tesselation permet de lire un nombre limité de sommets depuis la mémoire vidéo, mais ajoute des sommets supplémentaires dans les unités de gestion de la géométrie. Les détails géométriques ajoutés par la tesselation demandent donc de la puissance de calcul, mais réduisent les accès mémoire. ===L'historique de la tesselation sur les cartes 3D=== Les premières tentatives utilisaient des algorithmes matériels de tesselation, et non des ''shaders''. Par exemple, la première carte graphique commerciale avec de la tesselation matérielle était la Radeon 8500, de l'entreprise ATI (aujourd'hui rachetée par AMD), avec la technologie de tesselation TrueForm. Elle utilisait un circuit non-programmable, qui tessellait certaines surfaces et interpolait la forme de la surface entre les sommets. ATI améliora ensuite le TrueForm pour que des informations de tesselation soient lues depuis une texture, ce qui permet une implémentation de la technologie dite du ''displacement mapping''. En même temps, Matrox ajouta un algorithme de tesselation basé sur la technique de N-patch dans ses cartes graphiques. Mais ces techniques se basaient sur des algorithmes matériels non-programmables, ce qui rendait ces technologies insuffisamment flexibles et impraticables. De plus, c'était des technologies propriétaires, que les autres fabricants de cartes graphiques n'ont pas adopté. Elles sont donc tombées en désuétude. La tesselation a eu un regain d'intérêt à l'arrivée des '''''geometry shaders''''' dans DirectX 10 et OpenGL 3.2. Et il y avait de quoi, de tels shaders pouvant en théorie implémenter une forme limitée de tesselation. Mais le cout en performance est trop important, sans compter que les limitations de ces shaders n'a pas permis leur usage pour de la tesselation généraliste. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | DirectX 9 |- | class="f_rouge" | ''Input assembly'' | colspan="2" | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="4" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Une nouvelle étape a été franchie avec l'AMD Radeon HD2000 et le GPU de la Xbox 360, qui permettaient une tesselation partiellement programmable. La tesselation se faisait en deux étapes : une étape de découpage des triangles et une étape de modification des sommets créés. La première étape était un algorithme matériel configurable mais non-programmable, alors que la seconde était programmable. Mais le manque de support logiciel, le fait qu'on ne pouvait pas utiliser la tesselation en même temps que les ''geometry shader'', ainsi que la non-utilisation de cette technique par NVIDIA, a fait que cette technique n'a pas été reprise dans les GPU suivants. Il fallut attendre l'arrivée des '''tesselation shaders''' dans OpenGL 4.0 et DirectX 11 pour que des shaders adéquats arrivent sur le marché commercial. La tesselation sur ces cartes graphiques se fait en trois étapes : deux ''shaders'' et un algorithme matériel fixe entre les deux. Dans le détail, un ''hull shader'' est suivi par un étage fixe de tesselation, lui-même suivi par un ''domain shader''. L'étage fixe est là où se situe le découpage des triangles par l'unité matérielle configurable. La tesselation est suivie par la modification de la place des vertices créées, mais il y a aussi un shader avant la génération des nouveaux triangles. {|class="wikitable" |- ! colspan="7" | Avant DirectX 11 |- | class="f_rouge" | ''Input assembly'' | colspan="4" | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Les ''geometry shaders'' et les ''tesselation shaders'' étaient très limités, ce qui fait qu'ils ont été peu utilisés. Les programmeurs avaient beaucoup de mal à les utiliser de manière performante, sans compter que ces ''shaders'' s'intégraient très mal au pipeline graphique existant. Les cartes graphiques avaient du mal à les intégrer au hardware, sauf à recourir à des méthodes quelque peu tordues, comme on le verra dans ce qui suit. Pour résumer, le pipeline géométrique des PC est donc assez différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique a beaucoup évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes, pour quasiment disparaitre sur les GPU modernes. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Et de nombreuses étapes ont été rajoutées. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} 8g8igmy26poiem7r4ap1y0fzk8ag5cf 763457 763450 2026-04-11T15:59:42Z Mewtow 31375 Mewtow a déplacé la page [[Les cartes graphiques/Le pipeline géométrique d'avant DirectX 10]] vers [[Les cartes graphiques/Le pipeline géométrique : évolution]] 763450 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. Pour rappel, le pipeline géométrique regroupe les étapes suivantes : * L'étape de '''transformation''' effectue des changements de coordonnées pour chaque sommet. Premièrement, elle place les objets au bon endroit dans la scène 3D, ce qui demande de mettre à jour les coordonnées de chaque sommet de chaque modèle. C'est la première étape de calcul : l'''étape de transformation des modèles 3D''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * La phase d''''éclairage''' (en anglais ''lighting'') attribue une couleur à chaque sommet, qui dit si le sommet est fortement éclairé ou dans l'ombre. * La phase d''''assemblage des primitives''' regroupe les sommets en triangles. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la Geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée l''''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. En plus de l'unité de T&L, il y avait deux circuits supplémentaires : un ''input assembler'' et un circuit d'assemblage des primitives. L'''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} Les autres cartes graphiques de l'époque avaient une implémentation similaire, sur PC et console. Soit elles ne faisaient pas de calculs géométriques, soit elles avaient une unité de T&L. Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. La Geforce 3 a remplacé l'unité de T&L par un processeur de ''vertex shader'' programmable. Il s'occupait donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les autres GPU de l'époque ont suivi le mouvement. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. Mais DirectX 10 a changé la donne, avec l'introduction des ''geometry shader''. Nous détaillerons ces shaders dans le prochain chapitre. Tout ce que nous dirons est que ces shaders ne travaillent pas sur des sommets isolés, mais travaillent sur des primitives, des triangles. Ils peuvent ajouter, retirer ou modifier des triangles. Mais le résultat était assez désastreux. Les ''geometry shaders'' étaient trop limitées et les programmeurs avaient beaucoup de mal à les utiliser, ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Par la suite, des shaders dédiés aux techniques de tesselations ont été introduits. Nous verrons cela dans quelques chapitres, mais les techniques de tesselations visent à augmenter les détails géométriques d'un modèle 3D, en ajoutant des sommets à la volée. Des shaders dédiés ajoutent des sommets à un modèle, en suivant un certain algorithme. {|class="wikitable" |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. Ces shaders sont des ''geometry shaders'' améliorés, à savoir qu'ils peuvent travailler sur des primitives directement, voire sur des paquets de primitives. Concrètement, ils reçoivent des paquets de 32 sommets et exécutent des calculs dessus. Les calculs ne traitent pas chaque sommet isolément, ce qui permet de travailler sur des plusieurs triangles à la fois. Avec de tels shaders, les étapes d'assemblage de primitives et d'''input assembly'' sont inutiles, et ont disparues. Avec les ''primitive shaders'', l'implémentation exacte dépend de si la tesselation est activée ou non. Si la tesselation n'est pas activée, le ''vertex shader'' et le ''geométry shader'' sont fusionnés en un seul ''primitive shader''. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, sans tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="4" | |- ! DirectX 12 | colspan="4" | ''Primitive shader'' (AMD) |} Avec la tesselation activée, les ''geometry shaders'' et les ''domain shaders'' en un seul ''shader''. De même, les ''vertex shaders'' et les ''hull shaders'' sont fusionnés en un seul ''shader'', nommé l{{'}}''amplification shader''. Ainsi, le pipeline graphique est grandement simplifié, avec seulement deux ''shaders'' et un étage fixe, au lieu de quatre ''shaders'' différents. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, avec tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="7" | |- ! DirectX 12 | colspan="3" | * ''Amplification shader'' (AMD) | class="f_rouge" | Tesselation | colspan="3" | * ''Primitive shader'' (AMD) |} ==La tessellation== La '''tessellation''' est une technique qui permet d'ajouter des triangles à une surface à la volée. Les techniques de tesselation décomposent chaque triangle en sous-triangles plus petits, et modifient les coordonnées des sommets créés lors de ce processus. L'algorithme de découpage des triangles et la modification des coordonnées varie beaucoup selon la carte graphique ou le logiciel utilisé. Typiquement, les cartes graphiques actuelles ont un algorithme matériel pour le découpage des triangles qui est juste configurable, mais la modification des coordonnées des nouveaux sommets est programmable depuis DirectX 11. [[File:Tesselation pipeline.svg|centre|vignette|upright=2.0|Tessellation.]] Elle permet d'obtenir un bon niveau de détail géométrique, sans pour autant remplir la mémoire vidéo de sommets pré-calculés. Lire des sommets depuis la mémoire vidéo est une opération couteuse, même si les caches de sommets limitent la casse. La tesselation permet de lire un nombre limité de sommets depuis la mémoire vidéo, mais ajoute des sommets supplémentaires dans les unités de gestion de la géométrie. Les détails géométriques ajoutés par la tesselation demandent donc de la puissance de calcul, mais réduisent les accès mémoire. ===L'historique de la tesselation sur les cartes 3D=== Les premières tentatives utilisaient des algorithmes matériels de tesselation, et non des ''shaders''. Par exemple, la première carte graphique commerciale avec de la tesselation matérielle était la Radeon 8500, de l'entreprise ATI (aujourd'hui rachetée par AMD), avec la technologie de tesselation TrueForm. Elle utilisait un circuit non-programmable, qui tessellait certaines surfaces et interpolait la forme de la surface entre les sommets. ATI améliora ensuite le TrueForm pour que des informations de tesselation soient lues depuis une texture, ce qui permet une implémentation de la technologie dite du ''displacement mapping''. En même temps, Matrox ajouta un algorithme de tesselation basé sur la technique de N-patch dans ses cartes graphiques. Mais ces techniques se basaient sur des algorithmes matériels non-programmables, ce qui rendait ces technologies insuffisamment flexibles et impraticables. De plus, c'était des technologies propriétaires, que les autres fabricants de cartes graphiques n'ont pas adopté. Elles sont donc tombées en désuétude. La tesselation a eu un regain d'intérêt à l'arrivée des '''''geometry shaders''''' dans DirectX 10 et OpenGL 3.2. Et il y avait de quoi, de tels shaders pouvant en théorie implémenter une forme limitée de tesselation. Mais le cout en performance est trop important, sans compter que les limitations de ces shaders n'a pas permis leur usage pour de la tesselation généraliste. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | DirectX 9 |- | class="f_rouge" | ''Input assembly'' | colspan="2" | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="4" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Une nouvelle étape a été franchie avec l'AMD Radeon HD2000 et le GPU de la Xbox 360, qui permettaient une tesselation partiellement programmable. La tesselation se faisait en deux étapes : une étape de découpage des triangles et une étape de modification des sommets créés. La première étape était un algorithme matériel configurable mais non-programmable, alors que la seconde était programmable. Mais le manque de support logiciel, le fait qu'on ne pouvait pas utiliser la tesselation en même temps que les ''geometry shader'', ainsi que la non-utilisation de cette technique par NVIDIA, a fait que cette technique n'a pas été reprise dans les GPU suivants. Il fallut attendre l'arrivée des '''tesselation shaders''' dans OpenGL 4.0 et DirectX 11 pour que des shaders adéquats arrivent sur le marché commercial. La tesselation sur ces cartes graphiques se fait en trois étapes : deux ''shaders'' et un algorithme matériel fixe entre les deux. Dans le détail, un ''hull shader'' est suivi par un étage fixe de tesselation, lui-même suivi par un ''domain shader''. L'étage fixe est là où se situe le découpage des triangles par l'unité matérielle configurable. La tesselation est suivie par la modification de la place des vertices créées, mais il y a aussi un shader avant la génération des nouveaux triangles. {|class="wikitable" |- ! colspan="7" | Avant DirectX 11 |- | class="f_rouge" | ''Input assembly'' | colspan="4" | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Les ''geometry shaders'' et les ''tesselation shaders'' étaient très limités, ce qui fait qu'ils ont été peu utilisés. Les programmeurs avaient beaucoup de mal à les utiliser de manière performante, sans compter que ces ''shaders'' s'intégraient très mal au pipeline graphique existant. Les cartes graphiques avaient du mal à les intégrer au hardware, sauf à recourir à des méthodes quelque peu tordues, comme on le verra dans ce qui suit. Pour résumer, le pipeline géométrique des PC est donc assez différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique a beaucoup évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes, pour quasiment disparaitre sur les GPU modernes. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Et de nombreuses étapes ont été rajoutées. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique après DirectX 10 | nextText=Le pipeline géométrique après DirectX 10 }}{{autocat}} 8g8igmy26poiem7r4ap1y0fzk8ag5cf 763463 763457 2026-04-11T16:01:16Z Mewtow 31375 /* L'historique de la tesselation sur les cartes 3D */ 763463 wikitext text/x-wiki Le pipeline graphique a beaucoup changé dans le temps et est devenu de plus en plus programmable. Et cela s'est beaucoup ressentit dans la partie du pipeline graphique dédiée au traitement de la géométrie. De circuits fixes de T&L, il a intégré des ''vertex shaders'', puis des ''geometry shaders'', ''hull shaders'', ''domain shaders'', ''primitive shaders'', ''mesh shaders'' et bien d'autres. Dans ce chapitre, nous allons décrire l'évolution de ce pipeline et décrire comment elle s'est déroulée. L'évolution du pipeline géométrique s'est faite en plusieurs grandes périodes, qu'on peut distinguer très nettement. Et pour distinguer ces périodes, nous devons faire quelques rappels sur comment est calculée la géométrie d'une scène 3D. Pour rappel, le pipeline géométrique regroupe les étapes suivantes : * L'étape de '''transformation''' effectue des changements de coordonnées pour chaque sommet. Premièrement, elle place les objets au bon endroit dans la scène 3D, ce qui demande de mettre à jour les coordonnées de chaque sommet de chaque modèle. C'est la première étape de calcul : l'''étape de transformation des modèles 3D''. Ensuite, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * La phase d''''éclairage''' (en anglais ''lighting'') attribue une couleur à chaque sommet, qui dit si le sommet est fortement éclairé ou dans l'ombre. * La phase d''''assemblage des primitives''' regroupe les sommets en triangles. Chaque étape peut être prise en charge par un processeur de shader, ou par un circuit fixe spécialisé. Les étapes peuvent être regroupées. L'évolution dans le temps se traduit par des changements sur ce qui était programmables et ne l'était pas. Ceci étant dit, voyons comment ces 4 étapes ont variées dans le temps. Dans ce chapitre, nous allons voir l'ancien pipeline géométrique. Il ne traitait que des sommets, les triangles n'existaient qu'une fois l'assemblage de primitive effectué. Le pipeline géométrique après Direct X 10 fonctionne sur un principe totalement différent, mais ce sera le sujet du prochain chapitre. ==La période des ''mainframes'' et ''workstations'' : les années 70-90== La toute première période est celle des années 70 à 90, où le rendu 3D n'était possible que sur des ordinateurs très puissants, comme des ''mainframes'' et stations de travail. L'informatique grand public n'existait pas encore, et le rendu 3D était utilisé pour des simulateurs de vol pour l'armée, de l'imagerie médicale, des applications industrielles, l'architecture, etc. Les jeux vidéos n'existaient pas vraiment, sauf sur consoles et il n'y avait que du rendu 2D sur celles-ci. Les cartes graphiques étaient alors faites sur mesure pour un ordinateur bien précis, qui n'était vendu qu'à moins d'une centaine d'exemplaires. Les API graphiques n'existaient pas encore, ce qui fait que chaque modèle d'ordinateur faisait un peu à sa sauce. Les cartes graphiques étant réalisées presque sur mesure, il y avait une grand diversité d'implémentations, toutes différentes. Et n'oublions pas que tout était à concevoir, le rendu 3D n'en était qu'à ses débuts. Les toutes premières ne géraient pas le placage de textures, les fonctionnalités étaient limitées, la rastérisation utilisait des polygones, etc. ===Les processeurs à virgule flottante pour la géométrie=== Durant cette époque, le traitement de la géométrie était réalisé avec des processeurs à virgule flottante. Le chargement des sommets/triangles était le fait du processeur de commandes, le reste était le fait d'un ou de plusieurs processeurs. Il faut noter que les shaders n'existaient pas encore. Les processeurs étaient programmés avec un "microcode", qui est un terme trompeur pour ceux qui ont déjà lu des cours d'architecture des ordinateurs. En réalité, il s'agissait plus d'un ''firmware''. Les processeurs étaient connectés à une mémoire ROM, qui contenait un programme à exécuter. Le programme faisait tous les calculs nécessaires pour rendre un triangle/polygone. Parmi les processeurs utilisés dans cette période, on trouve le ''geometry engine'' de SGI, l'Intel i860, des processeurs Weitek, etc. Les processeurs en question avaient souvent des fonctionnalités utiles pour le rendu 3D. Au minimum, ils géraient quelques instructions utiles pour le rendu 3D, comme des opérations MAD (''Multiply And Accumulate''), des opérations trigonométriques, une opération pour calculer 1/x, des opérations transcendantales, etc. Le processeur Intel i860 était un processeur un peu particulier dans cette liste. Il gérait à la fois des calculs sur des entiers et à virgule flottante. Il disposait de deux modes : un mode normal où il fonctionnait comme un processeur classique, et un mode "VLIW" où il exécutait en même temps une opération flottante et une opération entière. Les instructions n'étaient pas encodées de la même manière dans les deux modes. ===Le ''geometry engine'' de SGI=== Le ''geometry engine'' de SGI était lui bien différent des autres processeurs. Il était conçu sur mesure pour les calculs géométriques. Le processeur était un processeur de type SIMD, c’est-à-dire qu'il faisait plusieurs opérations identiques, sur des opérandes différents. Il pouvait donc faire 4 additions/soustraction en même temps, chacune étant réalisée dans une unité de calcul séparée. Pour cela, il contenait 4 unités séparées, contenant chacune : * une unité de calcul capable de réaliser les 4 opérations ; * un registre flottant de 29 bits : un bit de signe, un exposant de 8 bits et une mantisse de 20 bits ; * une pile d'opérande de 8 niveaux, dont on reparlera dans la suite. L'unité de calcul gère naturellement les additions et soustraction. Elle dispose aussi d'un support limité des multiplications et divisions. Ces deux opérations sont réalisées en enchainant des additions/soustractions, avec l'aide du microcode. Pour simplifier l'implémentation, chaque unité de calcul contient trois registres entiers pour simplifier l'implémentation des multiplications et divisions. Ils sont appelés l'accumulateur, le ''shift up'' et le ''shift down''. Voyons à quoi ils servent. Pour rappel, une multiplication multiplie un multiplicande par un multiplieur. Le registre accumulateur mémorise les résultats intermédiaires de la multiplication/division, les fameux produits partiels. Les deux autres registres mémorisent le multiplieur ou le diviseur. Ils étaient décalés d'un rang par cycle d'horloge, ce qui facilitait l'implémentation des multiplications et divisions. Le registre ''shift up'' était décalé d'un rang vers la gauche, le registre ''shift down'' l'était d'un rang vers la droite. Les registres flottants sont en réalité chacun composés de deux registres séparés : un pour l'exposant, un autre pour les mantisses. Et l'unité de calcul était elle aussi découpée en deux : une portion gérait les exposants, l'autre les calculs sur les mantisses. Entre les deux, se trouvait toute la logique de contrôle du processeur, avec le décodeur, le ''program counter'', le microcode, etc. [[File:Exemple d'une machine à pile qui code ses entiers sur 4 Bytes.png|vignette|Exemple d'une pile/file d'opérandes, chaque opérande étant codée sur 4 Bytes.]] La pile d'opérande est une petite mémoire qui mémorise plusieurs opérandes. Les opérandes sont organisés en pile (LIFO), à savoir que les opérandes sont ajoutés les unes après l'autre dans cette pile, et que la mémoire mémorise leur ordre d'ajout. De plus, on ne peut accéder qu'à l'opérande la plus récemment ajouté. Il est possible de faire une analogie avec une pile d'assiette : les opérandes sont empilés comme des assiettes, seul l'opérande au sommet de cette pile est accessible. Le processeur gère plusieurs opérations. Premièrement, il peut ajouter un opérande au sommet de la pile, ou au contraire la retirer. Il s'agit des opérations PUSH et POP. Le processeur peut aussi faire une opération entre l’opérande dans les registres, et celle au sommet de la pile. L'opération retire l'opérande de la pile, et la remplace par le résultat de l’opération. La pile d'opérande permet de mémoriser une pile de matrice directement dans les 4 ''geometry engine''. Une matrice 4 par 4 est éclatée sur les différents ''geometry engine'' : ses 4 lignes sont réparties sur 4 processeurs, les 4 nombres par ligne sont dans les registres pour la pile. Il était initialement prévu de mettre cette pile de matrice en dehors du ''geometry engine'', dans une mémoire séparée. Mais Le pipeline géométrique était implémenté en utilisant 10/12 processeurs de ce type, enchainés l'un à la suite de l'autre. Les 4 premiers faisaient une multiplication par une matrice 4*4, les 6 suivants faisaient les opérations de ''clipping/culling'', les deux derniers faisaient la rastérisation proprement dite. Les quatre premiers processeurs prenaient en charge la multiplication d'un sommet par une matrice de 4 par 4. La matrice était découpée en 4 lignes ou colonnes, chacune étant mémorisée dans un processeur : 4 processeurs pour 4 lignes/colonnes, 4 nombres par ligne pour 4 registres flottants par processeur. La mise à jour de la matrice se faisait avec l'aide du processeur de commande. Il était responsable de la mise à jour simultanée des registres de tous les processeurs. Il disposait pour cela de plusieurs commandes, qui étaient toutes à destination des 4 premiers processeurs, sauf la dernière : * LoadMM pour charger une matrice dans les 16 registres des 4 processeurs géométriques ; * StoreMM pour sauvegarder cette matrice dans la RAM ; * MultMM pour multiplier la matrice avec une matrice au sommet de la pile ; * PushMM pour sauvegarder la matrice dans la pile ; * PopMM pour charger la matrice de la pile vers les 4 processeurs géométriques ; * LoadVP pour configurer les registres de ''viewport'' pour le ''clipping'' et la rastérisation. La multiplication demande de faire des additions et des multiplications. Le processeur avait bien une instruction de multiplication, mais celle-ci était réalisée en enchainant des additions et décalages à l'intérieur du processeur (l'instruction était microcodée). Il y avait le même problème pour les deux processeurs dédiés à la rastérisation. L'un était en charge de la coordonnée de profondeur, l'autre des coordonnées x et y à l'écran. Et les deux avaient besoin de faire des divisions, pour des histoires de correction de perspective détaillées dans un prochain chapitre sur la rastérisation. Les divisions étaient microcodées elles aussi, à savoir que le processeur les émulait avec des soustractions successives en interne. Les processeurs pour le ''clipping'' prenaient en charge le ''frustrum clipping'', à savoir qu'ils éliminaient ce qui était en-dehors du champ de vision. Pour rappel, le champ de vision est délimité par 6 plans : gauche, droite, haut, bas, ''near plane'', ''far plane''. Il y avait 6 processeurs pour 6 plans : chaque processeur clippait les triangles pour un plan bien précis. Il était possible de se passer du ''clipping'' pour le ''near'' et le ''far'' plane, ce qui réduisait le nombre de processeur à 5 ou 4. Le fait de clipper un plan à la fois peut paraitre étonnant, mais ce choix est justifié dans le document "Structuring a VLSI architecture", qui décrit le ''geometry engine'' en détail. La conception du chip s'est inspiré du circuit ''Clipping Divider'', utilisé dans le ''Line Drawing System-1'' de l'entreprise Evans & Sutherland. Le circuit en question ne faisait que du ''clipping'' et rien d'autre. Il faisait les calculs de ''clipping'' pour 4 plans en même temps et combinait les 4 résultats. Mais une implémentation similaire avec un circuit intégré aurait posé des problèmes de câblage, les interconnexions auraient pris trop de place. Une implémentation série/pipeline, qui clippe un plan à la fois, n'avait pas de problème. Elle permettait aussi de gérer à la fois des polygones et des lignes, si programmée correctement. : Une description complète du ''Geometry Engine'' et de la carte géométrique des IRIS est disponible ici : [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p127-clark.pdf The Geometry Engine : A VLSI Geometry System for Graphics]. ===La station de travail Appollo DN 10000 VS=== Les processeur de l'époque incorporaient diverses optimisations pour accélérer les calculs géométriques. Par exemple, la station de travail Appollo DN 10000 VS faisait les calculs géométriques et de ''clipping'' sur le processeur, pas sur la carte graphique. C'était un choix pas très courant pour l'époque, il n'est pas représentatif. La station de travail incorporait 4 processeurs principaux et une carte graphique qui s'occupait uniquement de la rastérisation et des textures. Les calculs géométriques étaient distribués sur les 4 processeurs, s'ils étaient libres. Toute la difficulté tenait à éviter que les 4 processeurs se marchent dessus. Il fallait synchroniser les processeur et la carte graphique ensemble. Une première synchronisation tient dans l'entrée des triangles dans les processeurs. Les processeurs traitent chaque triangle un par un. L'ordinateur a, dans sa RAM, un pointeur vers le prochain triangle. Les processeurs peuvent lire ce pointeur pour charger le prochain triangle à traiter, et l'incrémentent pour passer au triangle suivant. Mais il ne doit être modifié que par un seul processeur à la fois. Les programmes exécutés sur les 4 processeurs utilisent des mécanismes de synchronisation (des mutex/spinlock) pour éviter que deux processeurs tentent de modifier ce pointeur en même temps. Une autre synchronisation a lieu pour l'accès à la carte graphique. Quand un processeur a finit son travail, il envoie le triangle finalisé à la carte graphique. Mais elle ne peut recevoir qu'un triangle à la fois. Si deux processeurs finissent leur travail presque en même temps, ils vont vouloir envoyer leur résultat en même temps. Il y a alors un conflit d’accès à la carte graphique : plusieurs processeurs veulent communiquer avec en même temps. Là encore, un mécanisme de synchronisation est prévu, lui aussi logiciel et basé sur des mutex/spinlock/autres. Le processeur ajoutait des instructions spéciales pour accélérer le ''clipping''. Les opérations de ''clipping'' en question élimine les pixels situés en-dehors du champ de vision de la caméra, en-dehors du ''view frustrum''. Une implémentation naïve demande d'enchainer 6 paires de calculs, avec chacune une comparaison et un branchement. L'optimisation mémorise les résultats des comparaisons dans un registre de 6 bits (un bit par comparaison). Les 6 bits de résultats sont ensuite analysés par un unique branchement. Ainsi, on passe de 6 comparaisons + 6 branchements à 6 comparaisons + 1 branchement. ==Les cartes accélératrices des PC grand publics : les années 90-2000== La seconde période est celle de l'arrivée des accélérateurs 3D sur les PC grand public et les consoles. Au début de cette période, les cartes graphiques se débrouillaient sans circuits géométriques. Le calcul de la géométrie était réalisé sur le processeur, la carte graphique s'occupait de la rastérisation et de ce qui arrive après. Sur PC, la Geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée l''''unité de T&L''' (''Transform & Lighting''). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. En plus de l'unité de T&L, il y avait deux circuits supplémentaires : un ''input assembler'' et un circuit d'assemblage des primitives. L'''input assembler'' charge les vertices depuis la mémoire vidéo, l'assembleur de primitive regroupe les sommets en triangles avant la rastérisation. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | class="f_rouge" | ''Transform & Lighting'' | class="f_rouge" | ''Primitive assembly'' |} Les autres cartes graphiques de l'époque avaient une implémentation similaire, sur PC et console. Soit elles ne faisaient pas de calculs géométriques, soit elles avaient une unité de T&L. Précisons cependant une exception notable : la Nintendo 64, qui avait un processeur de shader pour la géométrie. La Geforce 3 a remplacé l'unité de T&L par un processeur de ''vertex shader'' programmable. Il s'occupait donc des phases de transformation et d'éclairage, comme les circuits de T&L qu'ils remplaçaient. Les autres GPU de l'époque ont suivi le mouvement. Les processeurs de ''shaders'' étaient toujours accompagnés de l''''input assembler'' et de l'assembleur de primitive. {|class="wikitable" |- ! colspan="4" | Après la Geforce 3, avant DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} S'en est suivi une longue période où le traitement de la géométrie se résumait aux ''vertex shaders''. Durant cette période, le pipeline graphique ne manipulait que des sommets, les triangles n'existaient qu'en sortie du pipeline géométrique. Mais DirectX 10 a changé la donne, avec l'introduction des ''geometry shader''. Nous détaillerons ces shaders dans le prochain chapitre. Tout ce que nous dirons est que ces shaders ne travaillent pas sur des sommets isolés, mais travaillent sur des primitives, des triangles. Ils peuvent ajouter, retirer ou modifier des triangles. Mais le résultat était assez désastreux. Les ''geometry shaders'' étaient trop limitées et les programmeurs avaient beaucoup de mal à les utiliser, ce qui fait que ces technologies ont été peu utilisées. {|class="wikitable" |- ! colspan="7" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | colspan="4" | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Par la suite, des shaders dédiés aux techniques de tesselations ont été introduits. Nous verrons cela dans quelques chapitres, mais les techniques de tesselations visent à augmenter les détails géométriques d'un modèle 3D, en ajoutant des sommets à la volée. Des shaders dédiés ajoutent des sommets à un modèle, en suivant un certain algorithme. {|class="wikitable" |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} L'arrivée des ''primitive shaders'' d'AMD et des ''mesh shaders'' de NVIDIA a modifié en profondeur le pipeline géométrique, qui est devenu plus simple, plus puissant et plus flexible. Ces shaders sont des ''geometry shaders'' améliorés, à savoir qu'ils peuvent travailler sur des primitives directement, voire sur des paquets de primitives. Concrètement, ils reçoivent des paquets de 32 sommets et exécutent des calculs dessus. Les calculs ne traitent pas chaque sommet isolément, ce qui permet de travailler sur des plusieurs triangles à la fois. Avec de tels shaders, les étapes d'assemblage de primitives et d'''input assembly'' sont inutiles, et ont disparues. Avec les ''primitive shaders'', l'implémentation exacte dépend de si la tesselation est activée ou non. Si la tesselation n'est pas activée, le ''vertex shader'' et le ''geométry shader'' sont fusionnés en un seul ''primitive shader''. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, sans tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="4" | |- ! DirectX 12 | colspan="4" | ''Primitive shader'' (AMD) |} Avec la tesselation activée, les ''geometry shaders'' et les ''domain shaders'' en un seul ''shader''. De même, les ''vertex shaders'' et les ''hull shaders'' sont fusionnés en un seul ''shader'', nommé l{{'}}''amplification shader''. Ainsi, le pipeline graphique est grandement simplifié, avec seulement deux ''shaders'' et un étage fixe, au lieu de quatre ''shaders'' différents. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, avec tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="7" | |- ! DirectX 12 | colspan="3" | * ''Amplification shader'' (AMD) | class="f_rouge" | Tesselation | colspan="3" | * ''Primitive shader'' (AMD) |} ==La tessellation== La '''tessellation''' est une technique qui permet d'ajouter des triangles à une surface à la volée. Les techniques de tesselation décomposent chaque triangle en sous-triangles plus petits, et modifient les coordonnées des sommets créés lors de ce processus. L'algorithme de découpage des triangles et la modification des coordonnées varie beaucoup selon la carte graphique ou le logiciel utilisé. Typiquement, les cartes graphiques actuelles ont un algorithme matériel pour le découpage des triangles qui est juste configurable, mais la modification des coordonnées des nouveaux sommets est programmable depuis DirectX 11. [[File:Tesselation pipeline.svg|centre|vignette|upright=2.0|Tessellation.]] Elle permet d'obtenir un bon niveau de détail géométrique, sans pour autant remplir la mémoire vidéo de sommets pré-calculés. Lire des sommets depuis la mémoire vidéo est une opération couteuse, même si les caches de sommets limitent la casse. La tesselation permet de lire un nombre limité de sommets depuis la mémoire vidéo, mais ajoute des sommets supplémentaires dans les unités de gestion de la géométrie. Les détails géométriques ajoutés par la tesselation demandent donc de la puissance de calcul, mais réduisent les accès mémoire. ===L'historique de la tesselation sur les cartes 3D=== Les premières tentatives utilisaient des algorithmes matériels de tesselation, et non des ''shaders''. Par exemple, la première carte graphique commerciale avec de la tesselation matérielle était la Radeon 8500, de l'entreprise ATI (aujourd'hui rachetée par AMD), avec la technologie de tesselation TrueForm. Elle utilisait un circuit non-programmable, qui tessellait certaines surfaces et interpolait la forme de la surface entre les sommets. ATI améliora ensuite le TrueForm pour que des informations de tesselation soient lues depuis une texture, ce qui permet une implémentation de la technologie dite du ''displacement mapping''. En même temps, Matrox ajouta un algorithme de tesselation basé sur la technique de N-patch dans ses cartes graphiques. Mais ces techniques se basaient sur des algorithmes matériels non-programmables, ce qui rendait ces technologies insuffisamment flexibles et impraticables. De plus, c'était des technologies propriétaires, que les autres fabricants de cartes graphiques n'ont pas adopté. Elles sont donc tombées en désuétude. La tesselation a eu un regain d'intérêt à l'arrivée des '''''geometry shaders''''' dans DirectX 10 et OpenGL 3.2. Et il y avait de quoi, de tels shaders pouvant en théorie implémenter une forme limitée de tesselation. Mais le cout en performance est trop important, sans compter que les limitations de ces shaders n'a pas permis leur usage pour de la tesselation généraliste. : Dans les tableaux qui vont suivre, les circuits non-programmables sont indiqués en rouge. {|class="wikitable" |- ! colspan="4" | DirectX 9 |- | class="f_rouge" | ''Input assembly'' | colspan="2" | ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="4" | DirectX 10 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Une nouvelle étape a été franchie avec l'AMD Radeon HD2000 et le GPU de la Xbox 360, qui permettaient une tesselation partiellement programmable. La tesselation se faisait en deux étapes : une étape de découpage des triangles et une étape de modification des sommets créés. La première étape était un algorithme matériel configurable mais non-programmable, alors que la seconde était programmable. Mais le manque de support logiciel, le fait qu'on ne pouvait pas utiliser la tesselation en même temps que les ''geometry shader'', ainsi que la non-utilisation de cette technique par NVIDIA, a fait que cette technique n'a pas été reprise dans les GPU suivants. Il fallut attendre l'arrivée des '''tesselation shaders''' dans OpenGL 4.0 et DirectX 11 pour que des shaders adéquats arrivent sur le marché commercial. La tesselation sur ces cartes graphiques se fait en trois étapes : deux ''shaders'' et un algorithme matériel fixe entre les deux. Dans le détail, un ''hull shader'' est suivi par un étage fixe de tesselation, lui-même suivi par un ''domain shader''. L'étage fixe est là où se situe le découpage des triangles par l'unité matérielle configurable. La tesselation est suivie par la modification de la place des vertices créées, mais il y a aussi un shader avant la génération des nouveaux triangles. {|class="wikitable" |- ! colspan="7" | Avant DirectX 11 |- | class="f_rouge" | ''Input assembly'' | colspan="4" | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- ! colspan="7" | DirectX 11 |- | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |} Les ''geometry shaders'' et les ''tesselation shaders'' étaient très limités, ce qui fait qu'ils ont été peu utilisés. Les programmeurs avaient beaucoup de mal à les utiliser de manière performante, sans compter que ces ''shaders'' s'intégraient très mal au pipeline graphique existant. Les cartes graphiques avaient du mal à les intégrer au hardware, sauf à recourir à des méthodes quelque peu tordues, comme on le verra dans ce qui suit. Pour résumer, le pipeline géométrique des PC est donc assez différent de ce qu'on avait sur les ''mainframes''. Le pipeline géométrique a beaucoup évolué dans le temps. Les étapes d'''input assembler'' et d'assemblage des primitives sont restées des circuits fixes, pour quasiment disparaitre sur les GPU modernes. Par contre, les étapes de transformation et d'éclairage ont elles beaucoup évoluées. Elles sont passées de circuits fixes à des circuits programmables. Et de nombreuses étapes ont été rajoutées. {{NavChapitre | book=Les cartes graphiques | prev=La répartition du travail sur les unités de shaders | prevText=La répartition du travail sur les unités de shaders | next=Le pipeline géométrique d'un GPU | nextText=Le pipeline géométrique d'un GPU }}{{autocat}} bc5v93bn1bzronxfgtw174lg8a6jaor Les cartes graphiques/Les Render Output Target 0 67394 763480 763298 2026-04-11T16:45:46Z Mewtow 31375 /* La gestion de la transparence : test alpha et alpha blending */ 763480 wikitext text/x-wiki Pour rappel, les étapes précédentes du pipeline graphiques manipulaient non pas des pixels, mais des fragments. Pour rappel, la distinction entre fragment et pixel est pertinente quand plusieurs objets sont l'un derrière l'autre. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. La couleur finale dépend de la couleur de tous ces points d'intersection. Intuitivement, l'objet le plus proche est censé cacher les autres et c'est donc lui qui décide de la couleur du pixel, mais cela demande de déterminer quel est l'objet le plus proche. De plus, certains objets sont transparents et la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Tout demande de calculer un pseudo-pixel pour chaque point d'intersection et de combiner leurs couleurs pour obtenir le résultat final. Les pseudo-pixels en question sont des '''fragments'''. Chaque fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont donc combinés pour obtenir la couleur finale de ce pixel. Pour résumer, la profondeur des fragments doit être gérée, de même que la transparence, etc. Et c'est justement le rôle de l'étage du pipeline que nous allons voir maintenant. Ces opérations sont réalisées dans un circuit qu'on nomme le '''Raster Operations Pipeline''' (ROP), aussi appelé ''Render Output Target'', situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. ==Les fonctions des ROP== Les ROP incorporent plusieurs fonctionnalités qui sont assez diverses. Leur seul lien est qu'il est préférable de les implémenter en matériel plutôt qu'en logiciel, et en-dehors des unités de textures. Il s'agit de fonctionnalités assez simples, basiques, mais nécessaires au fonctionnement de tout rendu 3D. Elles ont aussi pour particularité de beaucoup accéder à la mémoire vidéo. C'est la raison pour laquelle le ROP est situé en fin de pipeline, proche de la mémoire vidéo. Voyons quelles sont ces fonctionnalités. ===La gestion de la profondeur (tests de visibilité)=== Le premier rôle du ROP est de trier les fragments du plus proche au plus éloigné, pour gérer les situations où un triangle en cache un autre (quand un objet en cache un autre, par exemple). Prenons un mur rouge opaque qui cache un mur bleu. Dans ce cas, un pixel de l'écran sera associé à deux fragments : un pour le mur rouge, et un pour le bleu. Vu que le mur de devant est opaque, seul le fragment de ce mur doit être choisi : celui du mur qui est devant. Et il s'agit là d'un exemple simple, mais il est fréquent qu'un objet soit caché par plusieurs objets. En moyenne, un objet est caché par 3 à 4 objets dans un rendu 3d de jeu vidéo. Pour cela, chaque fragment a une coordonnée de profondeur, appelée la coordonnée z, qui indique la distance de ce fragment à la caméra. La coordonnée z est un nombre qui est d'autant plus petit que l'objet est près de l'écran. La profondeur est calculée à la rastérisation, ce qui fait que les ROP n'ont pas à la calculer, juste à trier les fragments en fonction de leur profondeur. [[File:Z-buffer no text.jpg|vignette|Z-buffer correspondant à un rendu]] Pour savoir quels fragments sont à éliminer (car cachés par d'autres), la carte graphique utilise ce qu'on appelle un '''tampon de profondeur'''. 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 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 fragment 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 et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, le fragment 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.]] ===La gestion de la transparence : test alpha et ''alpha blending''=== Les ROPs s'occupent aussi de la gestion de la transparence. La transparence/opacité d'un pixel/texel est codée par un nombre, la '''composante alpha''', qui est ajouté aux trois couleurs RGB. Plus la composante alpha est élevée, plus le pixel est opaque. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. La gestion de la transparence par les ROP est le fait de plusieurs fonctionnalités distinctes, les deux principales étant le test alpha et l'''alpha blending''. Le mélange ''alpha''gére les situations où on voit quelque chose à travers un objet transparent. Si un fragment transparent est placé devant un autre fragment, la couleur du pixel sera un mélange de la couleur du fragment transparent, et de la couleur du (ou des) fragments placé·s derrière. Le calcul à effectuer est très simple, et se limite en une simple moyenne pondérée par la transparence de la couleur des deux pixels. On parle alors d''''''alpha blending'''''. [[File:Texture splatting.png|centre|vignette|upright=2.0|Application de textures.]] L''''''alpha test''''' est une technique qui permet d'annuler le rendu d'un fragment en fonction de sa transparence. Si la composante alpha est en-dessous d'un seuil, le fragment est simplement abandonné. Chaque fragment passe une étape de test alpha qui vérifie si la valeur alpha est au-dessus de ce seuil ou non. S'il ne passe pas le test, le fragment est abandonné, il ne passe pas à l'étape de test de profondeur, ni aux étapes suivantes. Il s'agit d'une technique binaire de gestion de la transparence, qui est complétée par d'autres techniques. De nos jours, cette technologie est devenue obsolète. Elle optimisait le rendu de textures où les pixels sont soit totalement opaques, soit totalement transparents. Un exemple est le rendu du feuillage dans un jeu 3D : on a une texture de feuille plaquée sur un rectangle, les portions vertes étant totalement opaques et le reste étant totalement transparent. L'avantage est que cela évitait de mettre à jour le tampon de profondeur pour des fragments totalement transparents. Les fragments arrivant par paquets, calculés uns par uns par les unités de texture et de shaders, le calcul des couleurs est effectué progressivement. Pour cela, la carte graphique doit mettre en attente les résultats temporaires des mélanges pour chaque pixel. C'est le rôle du '''tampon de couleur''', l'équivalent du tampon de profondeur pour les couleurs des pixels. À chaque fragment reçu, le ROP lit la couleur du pixel associé dans le tampon de couleur, fait ou non la moyenne pondérée avec le fragment reçu et enregistre le résultat. Ces opérations de test et d'''alpha blending'' sont effectuées par un circuit spécialisé qui travaille en parallèle des circuits de calcul de la profondeur. Il faut noter que le rendu de la transparence se marie assez mal avec l'usage d'un tampon de profondeur. Le tampon de profondeur marche très bien quand on a des fragments totalement opaques : il a juste à mémoriser la coordonnée z du pixel le plus proche. Mais avec des fragments transparents, les choses sont plus compliquées, car plusieurs fragments sont censés être visibles, et on ne sait pas quelle coordonnée z stocker. L'interaction entre profondeur et transparence est réglée par diverses techniques. Avec l'''alpha blending'', c'est la cordonnée du fragment le plus proche qui est mémorisée dans le tampon de profondeur. ===Le tampon de ''stencil''=== Le '''''stencil''''' est une fonctionnalité des API graphiques et des cartes graphiques depuis déjà très longtemps. Il sert pour générer des effets graphiques très variés, qu'il serait vain de lister ici. Il a notamment été utilisé pour combattre le phénomène de ''z-fighting'' mentionné plus haut, il est utilisé pour calculer des ombres volumétriques (le moteur de DOOM 3 en faisait grand usage à la base), des réflexions simples, des lightmaps ou shadowmaps, et bien d'autres. Pour le résumer, on peut le voir comme une sorte de tampon de profondeur programmable, dans la coordonnée z est remplacée par une valeur arbitraire, dont le programmeur peut faire ce qu'il veut. La valeur est de plus une valeur entière, pas flottante. L'idée est que chaque pixel/fragment se voit attribuer une valeur entière, généralement codée sur un octet, que les programmeurs peuvent faire varier à loisir. L'octet ajouté est appelé l''''octet de ''stencil'''''. L'octet a une certaine valeur, qui est calculée par la carte graphique au fur et à mesure que les fragments sont traités. Il ne remplace pas la coordonnée de profondeur, mais s'ajoute à celle-ci. L'ensemble des octets de ''stencil'' est mémorisée dans un tableau en mémoire vidéo, avec un octet par pixel du ''framebuffer''. Le tableau porte le nom de '''tampon de ''stencil'''''. Il s'agit d'un tableau distinct du tampon de profondeur ou du tampon de couleur, du moins en théorie. Dans les faits, les techniques liées au tampon de ''stencil'' font souvent usage du tampon de profondeur, pour beaucoup d'effets graphiques avancés. Aussi, le tampon de ''stencil'' est souvent fusionné avec le tampon de profondeur. L'ensemble forme un tableau qui associe 32 bits à chaque" pixel : 24 bits pour une coordonnée z, 8 pour l'octet de ''stencil''. Chaque fragment a sa propre valeur de ''stencil'' qui est calculée par la carte graphique, généralement par les ''shaders''. Lors du passage d'un fragment les ROPs, la carte graphique lit le pixel associé dans le tampon de ''stencil''. Puis il compare l'octet de ''stencil'' avec celui du fragment traité. Si le test échoue, le fragment ne passe pas à l'étape de test de profondeur et est abandonné. S'il passe, le tampon de ''stencil'' est mis à jour. Par mis à jour, on veut dire que le ROP peut faire diverses manipulations dessus : l'incrémenter, le décrémenter, le mettre à 0, inverser ses bits, remplacer par l'octet de ''stencil'' du fragment, etc. Les opérations possibles sont bien plus nombreuses qu'avec le tampon de profondeur, qui se contente de remplacer la coordonnée z par celle du fragment. C'est toujours possible, on peut remplacer l'octet de ''stencil'' dans le tampon de ''stencil'' par celui du fragment s'il passe le test. Mais pour les techniques de rendu plus complexes, c'est une autre opération qui est utilisée, comme incrémenter l'octet dans le tampon de ''stencil''. ===Les effets de brouillard=== Les '''effets de brouillard''' sont des effets graphiques assez intéressants. Ils sont nécessaires dans certains jeux vidéo pour l'ambiance (pensez à des jeux d'horreur comme Silent Hill), mais ils ont surtout été utilisés pour économiser des calculs. L'idée est de ne pas calculer les graphismes au-delà d'une certaine distance, sans que cela se voie. L'idée est d'avoir un ''view frustum'' limité : le plan limite au-delà duquel on ne voit pas les objets est assez proche de la caméra. Mais si le plan limite est trop proche, cela donnera une cassure inesthétique dans le rendu. Pour masquer cette cassure, les programmeurs ajoutaient un effet de brouillard. Les objets au-delà du plan limite étaient totalement dans le brouillard, puis ce brouillard se réduisait progressivement en se rapprochant de la caméra, avant de s'annuler à partir d'une certaine distance. Pour calculer le brouillard, on mélange la couleur finale du pixel avec une ''couleur de brouillard'', la couleur de brouillard étant pondérée par la profondeur. Au-delà d'une certaine distance, l'objet est intégralement dans le brouillard : le brouillard domine totalement la couleur du pixel. En dessous d'une certaine distance, le brouillard est à zéro. Entre les deux, la couleur du brouillard et de l'objet devront toutes les deux être prises en compte dans les calculs. La formule de calcul exacte varie beaucoup, elle est souvent linéaire ou exponentielle. Notons que ce calcul implique à la fois de l'''alpha blending'' mais aussi la coordonnée de profondeur, ce qui en fait que son implémentation dans les ROPs est l'idéal. Aussi, les premières cartes graphiques calculaient le brouillard dans les ROP, en fonction de la coordonnée de profondeur du fragment. De nos jours, il est calculé par les ''pixel shaders'' et les ROP n'incorporent plus de technique de brouillard spécialisée. Vu que les pixels shaders peuvent s'en charger, cela fait moins de circuits dans les ROPs pour un cout en performance mineur. Et ce d'autant plus que les effets de brouillard sont devenus assez rares de nos jours. Autant les émuler dans les pixels shaders que d'utiliser des circuits pour une fonction devenue anecdotique. ===Les autres fonctions des ROPs=== Les ROPs gèrent aussi des techniques de '''''dithering''''', qui permettent d'adoucir des images lorsqu'elles sont redimensionnées et stockées avec une précision plus faible que la précision de calcul. Les ROPS implémentent aussi des techniques utilisées sur les ''blitters'' des anciennes cartes d'affichage 2D, comme l'application d''''opérations logiques''' sur chaque pixel enregistré dans le framebuffer. Les opérations logiques en question peuvent prendre une à deux opérandes. Les opérandes sont soit un pixel lu dans le ''framebuffer'', soit un fragment envoyé au ROP. Les opérations logiques à une opérande peuvent inverser, mettre à 0 ou à 1 le pixel dans le framebuffer, ou faire la même chose sur le fragment envoyé en opérande. Les opérations à deux opérandes lisent un pixel dans le framebuffer, et font un ET/OU/XOR avec le fragment opérande (une des deux opérandes peut être inversée). Elles sont utilisées pour faire du traitement d'image ou du rendu 2D, rarement pour du rendu 3D. Les ROPs gèrent aussi des '''masques d'écritures''', qui permettent de décider si un pixel doit être écrit ou non en mémoire. Il est possible d'inhiber certaines écritures dans le tampon de profondeur ou le tampon de couleur, éventuellement le tampon de stencil. Inhiber la mise à jour d'un pixel dans le tampon de profondeur est utile pour gérer la transparence. Si un pixel est transparent, même partiellement, il ne faut pas mettre à jour le tampon de profondeur, et cela peut être géré par ce système de masquage. Les masquages des couleurs permettent de ne modifier qu'une seule composante R/G/B au lieu de modifier les trois en même temps, pour faire certains effets visuels. ==L'architecture matérielle d'un ROP== Les ROP contiennent des circuits pour gérer la profondeur des fragments. Il effectuent un test de profondeur, à savoir que les fragments correspondant à un même pixel sont comparés pour savoir lequel est devant l'autre. Ils contiennent aussi des circuits pour gérer la transparence des fragments. Le ROP gère aussi l'antialiasing, de concert avec l'unité de rastérisation. D'autres fonctionnalités annexes sont parfois implémentées dans les ROP. Par exemple, les vielles cartes graphiques implémentaient les effets de brouillards dans les ROPs. Le tout est suivi d'une unité qui enregistre le résultat final en mémoire, où masques et opérations logiques sont appliqués. Les différentes opérations du ROP doivent se faire dans un certain ordre. Par exemple, gérer la transparence demande que les calculs de profondeur se fassent globalement après ou pendant l'''alpha blending''. Ou encore, les masques et opérations logiques se font à la toute fin du rendu. L'ordre des opérations est censé être le suivant : test ''alpha'', test du ''stencil'', test de profondeur, ''alpha blending''. Du moins, la carte graphique doit donner l'impression que c'est le cas. Elle peut optimiser le tout en traitant le tampon de profondeur, de couleur et de ''stencil'' en même temps, mais donner les résultats adéquats. [[File:Render Output Pipeline-processor.png|centre|vignette|upright=2|Render Output Pipeline-processor]] [[File:GeForce 6800 Pixel blending.png|droite|thumb|R.O.P des GeForce 6800.]] Un ROP est typiquement organisé comme illustré ci-dessous et ci-contre. Il récupère les fragments calculés par les pixels shaders et/ou les unités de texture, via un circuit d'interconnexion spécialisé. Chaque ROP est connecté à toutes les unités de ''shader'', même si la connexion n'est pas forcément directe. Toute unité de ''shader'' peut envoyer des pixels à n'importe quel ROP. Les circuits d'interconnexion sont généralement des réseaux d'interconnexion de type ''crossbar'', comme illustré ci-contre (le premier rectangle rouge). Notons que les circuits de gestion de la profondeur et de la transparence sont séparés dans les schémas ci-contre et ci-dessous. Il s'agit là d'une commodité qui ne reflète pas forcément l'implémentation matérielle. Et si ces deux circuits sont séparés, ils communiquent entre eux, notamment pour gérer la profondeur des fragments transparents. Les circuits de gestion de la profondeur et de la couleur gèrent diverses techniques de compression pour économiser de la mémoire et de la bande passante mémoire. Ajoutons à cela que ces deux unités contiennent des caches spécialisés, qui permettent de réduire fortement les accès mémoires, très fréquents dans cette étape du pipeline graphique. Il est à noter que sur certaines cartes graphiques, l'unité en charge de calculer les couleurs peut aussi servir à effectuer des comparaisons de profondeur. Ainsi, si tous les fragments sont opaques, on peut traiter deux fragments à la fois. C'était le cas sur la Geforce FX de Nvidia, ce qui permettait à cette carte graphique d'obtenir de très bonnes performances dans le jeu DOOM3. ==Les optimisations intégrées aux ROPs== Le ROP effectue beaucoup de lectures et écritures en mémoire vidéo. Or, la bande passante mémoire est limitée, ce qui fait que le ROP est un goulot d'étranglement assez important pour le rendu 3D. Heureusement, de nombreuses optimisations permettent d'optimiser le tout. Elles agissent sur la lecture du tampon de profondeur, mais aussi sur le ''framebuffer''. ===Le ''fast clear'' du ''framebuffer''=== Une première optimisation porte sur le ''framebuffer''. Le ''framebuffer''est souvent réutilisé d'une image sur l'autre. Quand une image a été envoyée à l'écran, le ''framebuffer'' est remis à zéro pour accueillir une nouvelle image. Et ce avec ou sans ''double buffering''. La mise à zéro est censée se faire en remettant réellement le ''framebuffer'' à zéro, en écrivant des 0 pour chaque pixel du ''framebuffer''. Mais il y a moyen de s'en passer. Pour cela, l'idée est que le ''framebuffer'' est découpé en ''tiles'', des carrés de 4, 8, 16 pixels de côté. Les ''tiles'' ont généralement la même taille que les ''tiles'' utilisées pour la rastérisation, mais passons sur ce détail. L'idée est de mémoriser, pour chaque ''tile'', si elle est mise à 0 ou non. Il suffit de cela d'un seul bit par ''tile'', appelé le bit RESET. L'ensemble des bits RESET est mémorisé dans une petite mémoire SRAM, intégrée aux ROPs. Lorsqu'on souhaite remettre à zéro le ''framebuffer'', il suffit de mettre à 0 tous les bits RESET dans cette SRAM, pas besoin d’accéder à la mémoire vidéo. Avant toute lecture dans le ''framebuffer'', le ROP lit cette SRAM pour vérifier si la ''tile'' en question a été remise à 0. Si ce n'est pas le cas, il lit le pixel voulu depuis le ''framebuffer''. Mais si c'est le cas, alors le ROP ne fait pas la lecture et fournit un pixel à zéro à la place, qui est utilisé pour l'''alpha blending'' ou autre. La moindre écriture dans une ''tile'' met le bit RESET à 0 : la ''tile'' entière est considérée comme non-remise à zéro, même si un seul pixel a été modifié dedans. Notons que l'usage d'une granularité par ''tile'' est un compromis. On peut ne peut pas utiliser un bit par pixel, car cela demanderait d'utiliser une SRAM énorme. De même, utiliser un seul bit pour tout le ''framebuffer'' ruinerait totalement l'optimisation : le ''framebuffer'' entier serait considéré comme non-RESET dès la première écriture d'un pixel dedans, on ne sauverait qu'un nombre trop limité d'accès mémoire. ===La z-compression=== La technique de '''z-compression''' compresse le tampon de profondeur. Plus précisément, elle découpe le tampon de profondeur en ''tiles'', en blocs carrés, qui sont compressés séparément les uns des autres. La taille des ''tiles'' est souvent la même que celle utilisée par le rastériseur pour la rastérisation grossière. Par exemple, la ''z-compression'' des cartes graphiques ATI radeon 9800, découpait le tampon de profondeur en ''tiles'' de 8 * 8 fragments, et les encodait avec un algorithme nommé DDPCM (''Differential differential pulse code modulation''). Précisons que cette compression ne change pas la taille occupée par le tampon de profondeur, mais seulement la quantité de données lue/écrite. La raison est que les ''tiles'' doivent avoir une place fixe en mémoire. Par exemple, si une ''tile'' non-compressée prend 64 octets, on trouvera une ''tile'' tous les 64 octets en mémoire vidéo, afin de simplifier les calculs d'adresse, afin que le ROP sache facilement où se trouve la ''tile'' à lire/écrire. Avec une vraie compression, les ''tiles'' se trouveraient à des endroits très variables d'une image à l'autre. Par contre, la z-compression réduit la quantité de données écrite dans le tampon de profondeur. Par exemple, au lieu d'écrire une ''tile'' non-compressée de 64 octets, on écrira une ''tile'' de seulement 6 octets, les 58 octets restants étant pas lus ou écrits. On obtient un gain en performance, pas en mémoire. [[File:AMD HyperZ.svg|centre|vignette|upright=2|AMD HyperZ]] Le format de compression ajoute un bit par ''tile'', qui indique si elle est compressée ou non. Le bit qui indique si la ''tile'' est compressée permet de laisser certaines ''tiles'' non-compressés, dans le cas où la compression ne permet pas de gagner de la place. La compression ajoute souvent un second bit, qui indique si la ''tile'' est à zéro ou non, sur le même modèle que pour le ''framebuffer''. Il accélère la remise à zéro du tampon de profondeur. Au lieu de réellement remettre tout le tampon de profondeur à 0, il suffit de réécrire un bit par ''tile''. Le gain en nombre d'accès mémoire peut se révéler assez impressionnant. Les deux bits en question peuvent être placés à deux endroits différents. La première solution serait d'utiliser une portion de la mémoire vidéo, mais cela demanderait de faire deux lectures par accès au tampon de profondeur. La vraie solution est d'utiliser une SRAM reliée aux ROPs, qui est assez grande pour mémoriser tout le tampon de profondeur, du moins avec deux bits par ''tile''. ===Le cache de profondeur=== Une optimisation complémentaire ajoute une ou plusieurs mémoires caches dans le ROP, dans le circuit de profondeur. Ce '''cache de profondeur''' stocke des portions du tampon de profondeur qui ont été lues ou écrite récemment. Comme cela, pas besoin de les recharger plusieurs fois : on charge un bloc une fois pour toutes, et on le conserve pour gérer les fragments qui suivent. Sur certaines cartes graphiques, les données dans le cache de profondeur sont stockées sous forme compressées dans le cache de profondeur, là encore pour augmenter la taille effective du cache. D'autres cartes graphiques ont un cache qui stocke des données décompressées dans le cache de profondeur. Tout est question de compromis entre accès rapide au cache et augmentation de la taille du cache. Il faut savoir que les autres unités de la carte graphique peuvent lire le tampon de profondeur, en théorie. Cela peut servir pour certaines techniques de rendu, comme pour le ''shadowmapping''. De ce fait, il arrive que le cache de profondeur contienne des données qui sont copiées dans d'autres caches, comme les caches des processeurs de shaders. Le cache de profondeur n'est pas gardé cohérent avec les autres caches du GPU, ce qui signifie que les écritures dans le cache de profondeur ne sont pas propagées dans les autres caches du GPU. Si on modifie des données dans ce cache, les autres caches qui ont une copie de ces données auront une version périmée de la donnée. C'est souvent un problème, sauf dans le cas du cache de profondeur, pour lequel ce n'est pas nécessaire. Cela évite d'implémenter des techniques de cohérence des caches couteuses en circuits et en performance, alors qu'elles n'auraient pas d'intérêt dans ce cas précis. ===Le ''z-fast pass''=== Le ''z-fast pass'' améliore la performance des '''prépasses z''', une technique utilisée par de nombreux moteurs de jeux vidéo. L'idée est que le moteur de jeu effectue plusieurs passes, chacune faisant un truc précis, la prépasse z étant l'une de ces passes. Lors d'une prépasse z, le moteur de jeu calcule la scène 3D, rastérise l'image, et remplit le tampon de profondeur uniquement. Il le place pas de textures, ne calcule pas de pixels shaders, il se préoccupe uniquement des coordonnées de profondeur des pixels. Au final, le rendu ne donne que le tampon de profondeur, qui est utilisé par les passes suivantes. L'utilité est très variable, mais il y a deux raisons pour effectuer une prépasse z : la performance, mais aussi certains effets graphiques. Par exemple, les effets d'occlusion ambiante "''screen space''" utilisent le tampon de profondeur pour faire leur travail. Il en est de même pour les ''shadowmaps'', qui effectuent une prépasse z par ombre à afficher. Une autre utilisation est que cela permet d'utiliser élimination des pixels cachés très performante. On effectue une prépasse z pour calculer le tampon de profondeur final, qui est ensuite utilisé par les passes suivantes pour éliminer les pixels cachés. Ainsi, les pixels cachés ne sont pas texturés et pixel shadés, avec certitude. Toujours est-il qu'une prépasse z utilise les ROP "à moitié", dans le sens où seul le tampon de profondeur est utilisé, par la gestion des couleurs. Mais il se trouve que les circuits qui servent pour l''alpha blending'' peuvent être réutilisés pour faire les comparaisons de profondeur ! Le résultat est que les ROP peuvent fonctionner à double vitesse lors d'une prépasse z ! Cela demande cependant de concevoir les circuits du ROP pour en profiter. L'optimisation est parfois appelée le '''''z-fast pass'''''. Tous les GPU depuis la Geforce FX en sont capables. Il y a cependant quelques contraintes. Premièrement, le ROP doit être configuré de manière à n’accéder qu'au tampon de profondeur, ils ne doivent pas dessiner dans le ''framebuffer''. L'''alpha blending'' doit être désactivé, de même que l'alpha-test. D'autres contraintes supplémentaires sont parfois présentes, surtout sur les vieux GPUs. Par exemple, l'antialiasing doit être désactivé lors de la prépasse z. Et mine de rien, cela ne marche que pour les prépasses z pures. Par exemple, certaines techniques de rendu différé augmentent la prépasse z pour que celle-ci ne calcule pas que le tampon de profondeur, mais aussi d'autres informations comme les normales : elles ne profitent pas de cette optimisation. {{NavChapitre | book=Les cartes graphiques | prev=Les unités de texture | prevText=Les unités de texture | next=Le support matériel du lancer de rayons | nextText=Le support matériel du lancer de rayons }}{{autocat}} 1lf1nu3g4p3drwmemeh2hvfrqfodgqn 763482 763480 2026-04-11T16:47:40Z Mewtow 31375 /* La gestion de la transparence : test alpha et alpha blending */ 763482 wikitext text/x-wiki Pour rappel, les étapes précédentes du pipeline graphiques manipulaient non pas des pixels, mais des fragments. Pour rappel, la distinction entre fragment et pixel est pertinente quand plusieurs objets sont l'un derrière l'autre. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. La couleur finale dépend de la couleur de tous ces points d'intersection. Intuitivement, l'objet le plus proche est censé cacher les autres et c'est donc lui qui décide de la couleur du pixel, mais cela demande de déterminer quel est l'objet le plus proche. De plus, certains objets sont transparents et la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Tout demande de calculer un pseudo-pixel pour chaque point d'intersection et de combiner leurs couleurs pour obtenir le résultat final. Les pseudo-pixels en question sont des '''fragments'''. Chaque fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont donc combinés pour obtenir la couleur finale de ce pixel. Pour résumer, la profondeur des fragments doit être gérée, de même que la transparence, etc. Et c'est justement le rôle de l'étage du pipeline que nous allons voir maintenant. Ces opérations sont réalisées dans un circuit qu'on nomme le '''Raster Operations Pipeline''' (ROP), aussi appelé ''Render Output Target'', situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. ==Les fonctions des ROP== Les ROP incorporent plusieurs fonctionnalités qui sont assez diverses. Leur seul lien est qu'il est préférable de les implémenter en matériel plutôt qu'en logiciel, et en-dehors des unités de textures. Il s'agit de fonctionnalités assez simples, basiques, mais nécessaires au fonctionnement de tout rendu 3D. Elles ont aussi pour particularité de beaucoup accéder à la mémoire vidéo. C'est la raison pour laquelle le ROP est situé en fin de pipeline, proche de la mémoire vidéo. Voyons quelles sont ces fonctionnalités. ===La gestion de la profondeur (tests de visibilité)=== Le premier rôle du ROP est de trier les fragments du plus proche au plus éloigné, pour gérer les situations où un triangle en cache un autre (quand un objet en cache un autre, par exemple). Prenons un mur rouge opaque qui cache un mur bleu. Dans ce cas, un pixel de l'écran sera associé à deux fragments : un pour le mur rouge, et un pour le bleu. Vu que le mur de devant est opaque, seul le fragment de ce mur doit être choisi : celui du mur qui est devant. Et il s'agit là d'un exemple simple, mais il est fréquent qu'un objet soit caché par plusieurs objets. En moyenne, un objet est caché par 3 à 4 objets dans un rendu 3d de jeu vidéo. Pour cela, chaque fragment a une coordonnée de profondeur, appelée la coordonnée z, qui indique la distance de ce fragment à la caméra. La coordonnée z est un nombre qui est d'autant plus petit que l'objet est près de l'écran. La profondeur est calculée à la rastérisation, ce qui fait que les ROP n'ont pas à la calculer, juste à trier les fragments en fonction de leur profondeur. [[File:Z-buffer no text.jpg|vignette|Z-buffer correspondant à un rendu]] Pour savoir quels fragments sont à éliminer (car cachés par d'autres), la carte graphique utilise ce qu'on appelle un '''tampon de profondeur'''. 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 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 fragment 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 et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, le fragment 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.]] ===La gestion de la transparence : test alpha et ''alpha blending''=== Les ROPs s'occupent aussi de la gestion de la transparence. La transparence/opacité d'un pixel/texel est codée par un nombre, la '''composante alpha''', qui est ajouté aux trois couleurs RGB. Plus la composante alpha est élevée, plus le pixel est opaque. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. La gestion de la transparence par les ROP est le fait de plusieurs fonctionnalités distinctes, les deux principales étant le test alpha et l'''alpha blending''. Le mélange ''alpha''gére les situations où on voit quelque chose à travers un objet transparent. Si un fragment transparent est placé devant un autre fragment, la couleur du pixel sera un mélange de la couleur du fragment transparent, et de la couleur du (ou des) fragments placé·s derrière. Le calcul à effectuer est très simple, et se limite en une simple moyenne pondérée par la transparence de la couleur des deux pixels. On parle alors d''''''alpha blending'''''. [[File:Texture splatting.png|centre|vignette|upright=2.0|Application de textures.]] L''''''alpha test''''' est une technique qui permet d'annuler le rendu d'un fragment en fonction de sa transparence. Si la composante alpha est en-dessous d'un seuil, le fragment est simplement abandonné. Il s'agit d'une technique binaire de gestion de la transparence, qui est complétée par d'autres techniques. Elle optimisait le rendu de textures où les pixels sont soit totalement opaques, soit totalement transparents. Un exemple est le rendu du feuillage dans un jeu 3D : on a une texture de feuille plaquée sur un rectangle, les portions vertes étant totalement opaques et le reste étant totalement transparent. L'avantage est que cela évitait de mettre à jour le tampon de profondeur pour des fragments totalement transparents. Les fragments arrivant par paquets, calculés uns par uns par les unités de texture et de shaders, le calcul des couleurs est effectué progressivement. Pour cela, la carte graphique doit mettre en attente les résultats temporaires des mélanges pour chaque pixel. C'est le rôle du '''tampon de couleur''', l'équivalent du tampon de profondeur pour les couleurs des pixels. À chaque fragment reçu, le ROP lit la couleur du pixel associé dans le tampon de couleur, fait ou non la moyenne pondérée avec le fragment reçu et enregistre le résultat. Ces opérations de test et d'''alpha blending'' sont effectuées par un circuit spécialisé qui travaille en parallèle des circuits de calcul de la profondeur. Il faut noter que le rendu de la transparence se marie assez mal avec l'usage d'un tampon de profondeur. Le tampon de profondeur marche très bien quand on a des fragments totalement opaques : il a juste à mémoriser la coordonnée z du pixel le plus proche. Mais avec des fragments transparents, les choses sont plus compliquées, car plusieurs fragments sont censés être visibles, et on ne sait pas quelle coordonnée z stocker. L'interaction entre profondeur et transparence est réglée par diverses techniques. Avec l'''alpha blending'', c'est la cordonnée du fragment le plus proche qui est mémorisée dans le tampon de profondeur. ===Le tampon de ''stencil''=== Le '''''stencil''''' est une fonctionnalité des API graphiques et des cartes graphiques depuis déjà très longtemps. Il sert pour générer des effets graphiques très variés, qu'il serait vain de lister ici. Il a notamment été utilisé pour combattre le phénomène de ''z-fighting'' mentionné plus haut, il est utilisé pour calculer des ombres volumétriques (le moteur de DOOM 3 en faisait grand usage à la base), des réflexions simples, des lightmaps ou shadowmaps, et bien d'autres. Pour le résumer, on peut le voir comme une sorte de tampon de profondeur programmable, dans la coordonnée z est remplacée par une valeur arbitraire, dont le programmeur peut faire ce qu'il veut. La valeur est de plus une valeur entière, pas flottante. L'idée est que chaque pixel/fragment se voit attribuer une valeur entière, généralement codée sur un octet, que les programmeurs peuvent faire varier à loisir. L'octet ajouté est appelé l''''octet de ''stencil'''''. L'octet a une certaine valeur, qui est calculée par la carte graphique au fur et à mesure que les fragments sont traités. Il ne remplace pas la coordonnée de profondeur, mais s'ajoute à celle-ci. L'ensemble des octets de ''stencil'' est mémorisée dans un tableau en mémoire vidéo, avec un octet par pixel du ''framebuffer''. Le tableau porte le nom de '''tampon de ''stencil'''''. Il s'agit d'un tableau distinct du tampon de profondeur ou du tampon de couleur, du moins en théorie. Dans les faits, les techniques liées au tampon de ''stencil'' font souvent usage du tampon de profondeur, pour beaucoup d'effets graphiques avancés. Aussi, le tampon de ''stencil'' est souvent fusionné avec le tampon de profondeur. L'ensemble forme un tableau qui associe 32 bits à chaque" pixel : 24 bits pour une coordonnée z, 8 pour l'octet de ''stencil''. Chaque fragment a sa propre valeur de ''stencil'' qui est calculée par la carte graphique, généralement par les ''shaders''. Lors du passage d'un fragment les ROPs, la carte graphique lit le pixel associé dans le tampon de ''stencil''. Puis il compare l'octet de ''stencil'' avec celui du fragment traité. Si le test échoue, le fragment ne passe pas à l'étape de test de profondeur et est abandonné. S'il passe, le tampon de ''stencil'' est mis à jour. Par mis à jour, on veut dire que le ROP peut faire diverses manipulations dessus : l'incrémenter, le décrémenter, le mettre à 0, inverser ses bits, remplacer par l'octet de ''stencil'' du fragment, etc. Les opérations possibles sont bien plus nombreuses qu'avec le tampon de profondeur, qui se contente de remplacer la coordonnée z par celle du fragment. C'est toujours possible, on peut remplacer l'octet de ''stencil'' dans le tampon de ''stencil'' par celui du fragment s'il passe le test. Mais pour les techniques de rendu plus complexes, c'est une autre opération qui est utilisée, comme incrémenter l'octet dans le tampon de ''stencil''. ===Les effets de brouillard=== Les '''effets de brouillard''' sont des effets graphiques assez intéressants. Ils sont nécessaires dans certains jeux vidéo pour l'ambiance (pensez à des jeux d'horreur comme Silent Hill), mais ils ont surtout été utilisés pour économiser des calculs. L'idée est de ne pas calculer les graphismes au-delà d'une certaine distance, sans que cela se voie. L'idée est d'avoir un ''view frustum'' limité : le plan limite au-delà duquel on ne voit pas les objets est assez proche de la caméra. Mais si le plan limite est trop proche, cela donnera une cassure inesthétique dans le rendu. Pour masquer cette cassure, les programmeurs ajoutaient un effet de brouillard. Les objets au-delà du plan limite étaient totalement dans le brouillard, puis ce brouillard se réduisait progressivement en se rapprochant de la caméra, avant de s'annuler à partir d'une certaine distance. Pour calculer le brouillard, on mélange la couleur finale du pixel avec une ''couleur de brouillard'', la couleur de brouillard étant pondérée par la profondeur. Au-delà d'une certaine distance, l'objet est intégralement dans le brouillard : le brouillard domine totalement la couleur du pixel. En dessous d'une certaine distance, le brouillard est à zéro. Entre les deux, la couleur du brouillard et de l'objet devront toutes les deux être prises en compte dans les calculs. La formule de calcul exacte varie beaucoup, elle est souvent linéaire ou exponentielle. Notons que ce calcul implique à la fois de l'''alpha blending'' mais aussi la coordonnée de profondeur, ce qui en fait que son implémentation dans les ROPs est l'idéal. Aussi, les premières cartes graphiques calculaient le brouillard dans les ROP, en fonction de la coordonnée de profondeur du fragment. De nos jours, il est calculé par les ''pixel shaders'' et les ROP n'incorporent plus de technique de brouillard spécialisée. Vu que les pixels shaders peuvent s'en charger, cela fait moins de circuits dans les ROPs pour un cout en performance mineur. Et ce d'autant plus que les effets de brouillard sont devenus assez rares de nos jours. Autant les émuler dans les pixels shaders que d'utiliser des circuits pour une fonction devenue anecdotique. ===Les autres fonctions des ROPs=== Les ROPs gèrent aussi des techniques de '''''dithering''''', qui permettent d'adoucir des images lorsqu'elles sont redimensionnées et stockées avec une précision plus faible que la précision de calcul. Les ROPS implémentent aussi des techniques utilisées sur les ''blitters'' des anciennes cartes d'affichage 2D, comme l'application d''''opérations logiques''' sur chaque pixel enregistré dans le framebuffer. Les opérations logiques en question peuvent prendre une à deux opérandes. Les opérandes sont soit un pixel lu dans le ''framebuffer'', soit un fragment envoyé au ROP. Les opérations logiques à une opérande peuvent inverser, mettre à 0 ou à 1 le pixel dans le framebuffer, ou faire la même chose sur le fragment envoyé en opérande. Les opérations à deux opérandes lisent un pixel dans le framebuffer, et font un ET/OU/XOR avec le fragment opérande (une des deux opérandes peut être inversée). Elles sont utilisées pour faire du traitement d'image ou du rendu 2D, rarement pour du rendu 3D. Les ROPs gèrent aussi des '''masques d'écritures''', qui permettent de décider si un pixel doit être écrit ou non en mémoire. Il est possible d'inhiber certaines écritures dans le tampon de profondeur ou le tampon de couleur, éventuellement le tampon de stencil. Inhiber la mise à jour d'un pixel dans le tampon de profondeur est utile pour gérer la transparence. Si un pixel est transparent, même partiellement, il ne faut pas mettre à jour le tampon de profondeur, et cela peut être géré par ce système de masquage. Les masquages des couleurs permettent de ne modifier qu'une seule composante R/G/B au lieu de modifier les trois en même temps, pour faire certains effets visuels. ==L'architecture matérielle d'un ROP== Les ROP contiennent des circuits pour gérer la profondeur des fragments. Il effectuent un test de profondeur, à savoir que les fragments correspondant à un même pixel sont comparés pour savoir lequel est devant l'autre. Ils contiennent aussi des circuits pour gérer la transparence des fragments. Le ROP gère aussi l'antialiasing, de concert avec l'unité de rastérisation. D'autres fonctionnalités annexes sont parfois implémentées dans les ROP. Par exemple, les vielles cartes graphiques implémentaient les effets de brouillards dans les ROPs. Le tout est suivi d'une unité qui enregistre le résultat final en mémoire, où masques et opérations logiques sont appliqués. Les différentes opérations du ROP doivent se faire dans un certain ordre. Par exemple, gérer la transparence demande que les calculs de profondeur se fassent globalement après ou pendant l'''alpha blending''. Ou encore, les masques et opérations logiques se font à la toute fin du rendu. L'ordre des opérations est censé être le suivant : test ''alpha'', test du ''stencil'', test de profondeur, ''alpha blending''. Du moins, la carte graphique doit donner l'impression que c'est le cas. Elle peut optimiser le tout en traitant le tampon de profondeur, de couleur et de ''stencil'' en même temps, mais donner les résultats adéquats. [[File:Render Output Pipeline-processor.png|centre|vignette|upright=2|Render Output Pipeline-processor]] [[File:GeForce 6800 Pixel blending.png|droite|thumb|R.O.P des GeForce 6800.]] Un ROP est typiquement organisé comme illustré ci-dessous et ci-contre. Il récupère les fragments calculés par les pixels shaders et/ou les unités de texture, via un circuit d'interconnexion spécialisé. Chaque ROP est connecté à toutes les unités de ''shader'', même si la connexion n'est pas forcément directe. Toute unité de ''shader'' peut envoyer des pixels à n'importe quel ROP. Les circuits d'interconnexion sont généralement des réseaux d'interconnexion de type ''crossbar'', comme illustré ci-contre (le premier rectangle rouge). Notons que les circuits de gestion de la profondeur et de la transparence sont séparés dans les schémas ci-contre et ci-dessous. Il s'agit là d'une commodité qui ne reflète pas forcément l'implémentation matérielle. Et si ces deux circuits sont séparés, ils communiquent entre eux, notamment pour gérer la profondeur des fragments transparents. Les circuits de gestion de la profondeur et de la couleur gèrent diverses techniques de compression pour économiser de la mémoire et de la bande passante mémoire. Ajoutons à cela que ces deux unités contiennent des caches spécialisés, qui permettent de réduire fortement les accès mémoires, très fréquents dans cette étape du pipeline graphique. Il est à noter que sur certaines cartes graphiques, l'unité en charge de calculer les couleurs peut aussi servir à effectuer des comparaisons de profondeur. Ainsi, si tous les fragments sont opaques, on peut traiter deux fragments à la fois. C'était le cas sur la Geforce FX de Nvidia, ce qui permettait à cette carte graphique d'obtenir de très bonnes performances dans le jeu DOOM3. ==Les optimisations intégrées aux ROPs== Le ROP effectue beaucoup de lectures et écritures en mémoire vidéo. Or, la bande passante mémoire est limitée, ce qui fait que le ROP est un goulot d'étranglement assez important pour le rendu 3D. Heureusement, de nombreuses optimisations permettent d'optimiser le tout. Elles agissent sur la lecture du tampon de profondeur, mais aussi sur le ''framebuffer''. ===Le ''fast clear'' du ''framebuffer''=== Une première optimisation porte sur le ''framebuffer''. Le ''framebuffer''est souvent réutilisé d'une image sur l'autre. Quand une image a été envoyée à l'écran, le ''framebuffer'' est remis à zéro pour accueillir une nouvelle image. Et ce avec ou sans ''double buffering''. La mise à zéro est censée se faire en remettant réellement le ''framebuffer'' à zéro, en écrivant des 0 pour chaque pixel du ''framebuffer''. Mais il y a moyen de s'en passer. Pour cela, l'idée est que le ''framebuffer'' est découpé en ''tiles'', des carrés de 4, 8, 16 pixels de côté. Les ''tiles'' ont généralement la même taille que les ''tiles'' utilisées pour la rastérisation, mais passons sur ce détail. L'idée est de mémoriser, pour chaque ''tile'', si elle est mise à 0 ou non. Il suffit de cela d'un seul bit par ''tile'', appelé le bit RESET. L'ensemble des bits RESET est mémorisé dans une petite mémoire SRAM, intégrée aux ROPs. Lorsqu'on souhaite remettre à zéro le ''framebuffer'', il suffit de mettre à 0 tous les bits RESET dans cette SRAM, pas besoin d’accéder à la mémoire vidéo. Avant toute lecture dans le ''framebuffer'', le ROP lit cette SRAM pour vérifier si la ''tile'' en question a été remise à 0. Si ce n'est pas le cas, il lit le pixel voulu depuis le ''framebuffer''. Mais si c'est le cas, alors le ROP ne fait pas la lecture et fournit un pixel à zéro à la place, qui est utilisé pour l'''alpha blending'' ou autre. La moindre écriture dans une ''tile'' met le bit RESET à 0 : la ''tile'' entière est considérée comme non-remise à zéro, même si un seul pixel a été modifié dedans. Notons que l'usage d'une granularité par ''tile'' est un compromis. On peut ne peut pas utiliser un bit par pixel, car cela demanderait d'utiliser une SRAM énorme. De même, utiliser un seul bit pour tout le ''framebuffer'' ruinerait totalement l'optimisation : le ''framebuffer'' entier serait considéré comme non-RESET dès la première écriture d'un pixel dedans, on ne sauverait qu'un nombre trop limité d'accès mémoire. ===La z-compression=== La technique de '''z-compression''' compresse le tampon de profondeur. Plus précisément, elle découpe le tampon de profondeur en ''tiles'', en blocs carrés, qui sont compressés séparément les uns des autres. La taille des ''tiles'' est souvent la même que celle utilisée par le rastériseur pour la rastérisation grossière. Par exemple, la ''z-compression'' des cartes graphiques ATI radeon 9800, découpait le tampon de profondeur en ''tiles'' de 8 * 8 fragments, et les encodait avec un algorithme nommé DDPCM (''Differential differential pulse code modulation''). Précisons que cette compression ne change pas la taille occupée par le tampon de profondeur, mais seulement la quantité de données lue/écrite. La raison est que les ''tiles'' doivent avoir une place fixe en mémoire. Par exemple, si une ''tile'' non-compressée prend 64 octets, on trouvera une ''tile'' tous les 64 octets en mémoire vidéo, afin de simplifier les calculs d'adresse, afin que le ROP sache facilement où se trouve la ''tile'' à lire/écrire. Avec une vraie compression, les ''tiles'' se trouveraient à des endroits très variables d'une image à l'autre. Par contre, la z-compression réduit la quantité de données écrite dans le tampon de profondeur. Par exemple, au lieu d'écrire une ''tile'' non-compressée de 64 octets, on écrira une ''tile'' de seulement 6 octets, les 58 octets restants étant pas lus ou écrits. On obtient un gain en performance, pas en mémoire. [[File:AMD HyperZ.svg|centre|vignette|upright=2|AMD HyperZ]] Le format de compression ajoute un bit par ''tile'', qui indique si elle est compressée ou non. Le bit qui indique si la ''tile'' est compressée permet de laisser certaines ''tiles'' non-compressés, dans le cas où la compression ne permet pas de gagner de la place. La compression ajoute souvent un second bit, qui indique si la ''tile'' est à zéro ou non, sur le même modèle que pour le ''framebuffer''. Il accélère la remise à zéro du tampon de profondeur. Au lieu de réellement remettre tout le tampon de profondeur à 0, il suffit de réécrire un bit par ''tile''. Le gain en nombre d'accès mémoire peut se révéler assez impressionnant. Les deux bits en question peuvent être placés à deux endroits différents. La première solution serait d'utiliser une portion de la mémoire vidéo, mais cela demanderait de faire deux lectures par accès au tampon de profondeur. La vraie solution est d'utiliser une SRAM reliée aux ROPs, qui est assez grande pour mémoriser tout le tampon de profondeur, du moins avec deux bits par ''tile''. ===Le cache de profondeur=== Une optimisation complémentaire ajoute une ou plusieurs mémoires caches dans le ROP, dans le circuit de profondeur. Ce '''cache de profondeur''' stocke des portions du tampon de profondeur qui ont été lues ou écrite récemment. Comme cela, pas besoin de les recharger plusieurs fois : on charge un bloc une fois pour toutes, et on le conserve pour gérer les fragments qui suivent. Sur certaines cartes graphiques, les données dans le cache de profondeur sont stockées sous forme compressées dans le cache de profondeur, là encore pour augmenter la taille effective du cache. D'autres cartes graphiques ont un cache qui stocke des données décompressées dans le cache de profondeur. Tout est question de compromis entre accès rapide au cache et augmentation de la taille du cache. Il faut savoir que les autres unités de la carte graphique peuvent lire le tampon de profondeur, en théorie. Cela peut servir pour certaines techniques de rendu, comme pour le ''shadowmapping''. De ce fait, il arrive que le cache de profondeur contienne des données qui sont copiées dans d'autres caches, comme les caches des processeurs de shaders. Le cache de profondeur n'est pas gardé cohérent avec les autres caches du GPU, ce qui signifie que les écritures dans le cache de profondeur ne sont pas propagées dans les autres caches du GPU. Si on modifie des données dans ce cache, les autres caches qui ont une copie de ces données auront une version périmée de la donnée. C'est souvent un problème, sauf dans le cas du cache de profondeur, pour lequel ce n'est pas nécessaire. Cela évite d'implémenter des techniques de cohérence des caches couteuses en circuits et en performance, alors qu'elles n'auraient pas d'intérêt dans ce cas précis. ===Le ''z-fast pass''=== Le ''z-fast pass'' améliore la performance des '''prépasses z''', une technique utilisée par de nombreux moteurs de jeux vidéo. L'idée est que le moteur de jeu effectue plusieurs passes, chacune faisant un truc précis, la prépasse z étant l'une de ces passes. Lors d'une prépasse z, le moteur de jeu calcule la scène 3D, rastérise l'image, et remplit le tampon de profondeur uniquement. Il le place pas de textures, ne calcule pas de pixels shaders, il se préoccupe uniquement des coordonnées de profondeur des pixels. Au final, le rendu ne donne que le tampon de profondeur, qui est utilisé par les passes suivantes. L'utilité est très variable, mais il y a deux raisons pour effectuer une prépasse z : la performance, mais aussi certains effets graphiques. Par exemple, les effets d'occlusion ambiante "''screen space''" utilisent le tampon de profondeur pour faire leur travail. Il en est de même pour les ''shadowmaps'', qui effectuent une prépasse z par ombre à afficher. Une autre utilisation est que cela permet d'utiliser élimination des pixels cachés très performante. On effectue une prépasse z pour calculer le tampon de profondeur final, qui est ensuite utilisé par les passes suivantes pour éliminer les pixels cachés. Ainsi, les pixels cachés ne sont pas texturés et pixel shadés, avec certitude. Toujours est-il qu'une prépasse z utilise les ROP "à moitié", dans le sens où seul le tampon de profondeur est utilisé, par la gestion des couleurs. Mais il se trouve que les circuits qui servent pour l''alpha blending'' peuvent être réutilisés pour faire les comparaisons de profondeur ! Le résultat est que les ROP peuvent fonctionner à double vitesse lors d'une prépasse z ! Cela demande cependant de concevoir les circuits du ROP pour en profiter. L'optimisation est parfois appelée le '''''z-fast pass'''''. Tous les GPU depuis la Geforce FX en sont capables. Il y a cependant quelques contraintes. Premièrement, le ROP doit être configuré de manière à n’accéder qu'au tampon de profondeur, ils ne doivent pas dessiner dans le ''framebuffer''. L'''alpha blending'' doit être désactivé, de même que l'alpha-test. D'autres contraintes supplémentaires sont parfois présentes, surtout sur les vieux GPUs. Par exemple, l'antialiasing doit être désactivé lors de la prépasse z. Et mine de rien, cela ne marche que pour les prépasses z pures. Par exemple, certaines techniques de rendu différé augmentent la prépasse z pour que celle-ci ne calcule pas que le tampon de profondeur, mais aussi d'autres informations comme les normales : elles ne profitent pas de cette optimisation. {{NavChapitre | book=Les cartes graphiques | prev=Les unités de texture | prevText=Les unités de texture | next=Le support matériel du lancer de rayons | nextText=Le support matériel du lancer de rayons }}{{autocat}} i1dfhsrf4ah1i9x48j67onu2kxyxq5k 763505 763482 2026-04-11T19:34:54Z Mewtow 31375 /* L'architecture matérielle d'un ROP */ 763505 wikitext text/x-wiki Pour rappel, les étapes précédentes du pipeline graphiques manipulaient non pas des pixels, mais des fragments. Pour rappel, la distinction entre fragment et pixel est pertinente quand plusieurs objets sont l'un derrière l'autre. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. La couleur finale dépend de la couleur de tous ces points d'intersection. Intuitivement, l'objet le plus proche est censé cacher les autres et c'est donc lui qui décide de la couleur du pixel, mais cela demande de déterminer quel est l'objet le plus proche. De plus, certains objets sont transparents et la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Tout demande de calculer un pseudo-pixel pour chaque point d'intersection et de combiner leurs couleurs pour obtenir le résultat final. Les pseudo-pixels en question sont des '''fragments'''. Chaque fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont donc combinés pour obtenir la couleur finale de ce pixel. Pour résumer, la profondeur des fragments doit être gérée, de même que la transparence, etc. Et c'est justement le rôle de l'étage du pipeline que nous allons voir maintenant. Ces opérations sont réalisées dans un circuit qu'on nomme le '''Raster Operations Pipeline''' (ROP), aussi appelé ''Render Output Target'', situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. ==Les fonctions des ROP== Les ROP incorporent plusieurs fonctionnalités qui sont assez diverses. Leur seul lien est qu'il est préférable de les implémenter en matériel plutôt qu'en logiciel, et en-dehors des unités de textures. Il s'agit de fonctionnalités assez simples, basiques, mais nécessaires au fonctionnement de tout rendu 3D. Elles ont aussi pour particularité de beaucoup accéder à la mémoire vidéo. C'est la raison pour laquelle le ROP est situé en fin de pipeline, proche de la mémoire vidéo. Voyons quelles sont ces fonctionnalités. ===La gestion de la profondeur (tests de visibilité)=== Le premier rôle du ROP est de trier les fragments du plus proche au plus éloigné, pour gérer les situations où un triangle en cache un autre (quand un objet en cache un autre, par exemple). Prenons un mur rouge opaque qui cache un mur bleu. Dans ce cas, un pixel de l'écran sera associé à deux fragments : un pour le mur rouge, et un pour le bleu. Vu que le mur de devant est opaque, seul le fragment de ce mur doit être choisi : celui du mur qui est devant. Et il s'agit là d'un exemple simple, mais il est fréquent qu'un objet soit caché par plusieurs objets. En moyenne, un objet est caché par 3 à 4 objets dans un rendu 3d de jeu vidéo. Pour cela, chaque fragment a une coordonnée de profondeur, appelée la coordonnée z, qui indique la distance de ce fragment à la caméra. La coordonnée z est un nombre qui est d'autant plus petit que l'objet est près de l'écran. La profondeur est calculée à la rastérisation, ce qui fait que les ROP n'ont pas à la calculer, juste à trier les fragments en fonction de leur profondeur. [[File:Z-buffer no text.jpg|vignette|Z-buffer correspondant à un rendu]] Pour savoir quels fragments sont à éliminer (car cachés par d'autres), la carte graphique utilise ce qu'on appelle un '''tampon de profondeur'''. 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 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 fragment 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 et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, le fragment 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.]] ===La gestion de la transparence : test alpha et ''alpha blending''=== Les ROPs s'occupent aussi de la gestion de la transparence. La transparence/opacité d'un pixel/texel est codée par un nombre, la '''composante alpha''', qui est ajouté aux trois couleurs RGB. Plus la composante alpha est élevée, plus le pixel est opaque. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. La gestion de la transparence par les ROP est le fait de plusieurs fonctionnalités distinctes, les deux principales étant le test alpha et l'''alpha blending''. Le mélange ''alpha''gére les situations où on voit quelque chose à travers un objet transparent. Si un fragment transparent est placé devant un autre fragment, la couleur du pixel sera un mélange de la couleur du fragment transparent, et de la couleur du (ou des) fragments placé·s derrière. Le calcul à effectuer est très simple, et se limite en une simple moyenne pondérée par la transparence de la couleur des deux pixels. On parle alors d''''''alpha blending'''''. [[File:Texture splatting.png|centre|vignette|upright=2.0|Application de textures.]] L''''''alpha test''''' est une technique qui permet d'annuler le rendu d'un fragment en fonction de sa transparence. Si la composante alpha est en-dessous d'un seuil, le fragment est simplement abandonné. Il s'agit d'une technique binaire de gestion de la transparence, qui est complétée par d'autres techniques. Elle optimisait le rendu de textures où les pixels sont soit totalement opaques, soit totalement transparents. Un exemple est le rendu du feuillage dans un jeu 3D : on a une texture de feuille plaquée sur un rectangle, les portions vertes étant totalement opaques et le reste étant totalement transparent. L'avantage est que cela évitait de mettre à jour le tampon de profondeur pour des fragments totalement transparents. Les fragments arrivant par paquets, calculés uns par uns par les unités de texture et de shaders, le calcul des couleurs est effectué progressivement. Pour cela, la carte graphique doit mettre en attente les résultats temporaires des mélanges pour chaque pixel. C'est le rôle du '''tampon de couleur''', l'équivalent du tampon de profondeur pour les couleurs des pixels. À chaque fragment reçu, le ROP lit la couleur du pixel associé dans le tampon de couleur, fait ou non la moyenne pondérée avec le fragment reçu et enregistre le résultat. Ces opérations de test et d'''alpha blending'' sont effectuées par un circuit spécialisé qui travaille en parallèle des circuits de calcul de la profondeur. Il faut noter que le rendu de la transparence se marie assez mal avec l'usage d'un tampon de profondeur. Le tampon de profondeur marche très bien quand on a des fragments totalement opaques : il a juste à mémoriser la coordonnée z du pixel le plus proche. Mais avec des fragments transparents, les choses sont plus compliquées, car plusieurs fragments sont censés être visibles, et on ne sait pas quelle coordonnée z stocker. L'interaction entre profondeur et transparence est réglée par diverses techniques. Avec l'''alpha blending'', c'est la cordonnée du fragment le plus proche qui est mémorisée dans le tampon de profondeur. ===Le tampon de ''stencil''=== Le '''''stencil''''' est une fonctionnalité des API graphiques et des cartes graphiques depuis déjà très longtemps. Il sert pour générer des effets graphiques très variés, qu'il serait vain de lister ici. Il a notamment été utilisé pour combattre le phénomène de ''z-fighting'' mentionné plus haut, il est utilisé pour calculer des ombres volumétriques (le moteur de DOOM 3 en faisait grand usage à la base), des réflexions simples, des lightmaps ou shadowmaps, et bien d'autres. Pour le résumer, on peut le voir comme une sorte de tampon de profondeur programmable, dans la coordonnée z est remplacée par une valeur arbitraire, dont le programmeur peut faire ce qu'il veut. La valeur est de plus une valeur entière, pas flottante. L'idée est que chaque pixel/fragment se voit attribuer une valeur entière, généralement codée sur un octet, que les programmeurs peuvent faire varier à loisir. L'octet ajouté est appelé l''''octet de ''stencil'''''. L'octet a une certaine valeur, qui est calculée par la carte graphique au fur et à mesure que les fragments sont traités. Il ne remplace pas la coordonnée de profondeur, mais s'ajoute à celle-ci. L'ensemble des octets de ''stencil'' est mémorisée dans un tableau en mémoire vidéo, avec un octet par pixel du ''framebuffer''. Le tableau porte le nom de '''tampon de ''stencil'''''. Il s'agit d'un tableau distinct du tampon de profondeur ou du tampon de couleur, du moins en théorie. Dans les faits, les techniques liées au tampon de ''stencil'' font souvent usage du tampon de profondeur, pour beaucoup d'effets graphiques avancés. Aussi, le tampon de ''stencil'' est souvent fusionné avec le tampon de profondeur. L'ensemble forme un tableau qui associe 32 bits à chaque" pixel : 24 bits pour une coordonnée z, 8 pour l'octet de ''stencil''. Chaque fragment a sa propre valeur de ''stencil'' qui est calculée par la carte graphique, généralement par les ''shaders''. Lors du passage d'un fragment les ROPs, la carte graphique lit le pixel associé dans le tampon de ''stencil''. Puis il compare l'octet de ''stencil'' avec celui du fragment traité. Si le test échoue, le fragment ne passe pas à l'étape de test de profondeur et est abandonné. S'il passe, le tampon de ''stencil'' est mis à jour. Par mis à jour, on veut dire que le ROP peut faire diverses manipulations dessus : l'incrémenter, le décrémenter, le mettre à 0, inverser ses bits, remplacer par l'octet de ''stencil'' du fragment, etc. Les opérations possibles sont bien plus nombreuses qu'avec le tampon de profondeur, qui se contente de remplacer la coordonnée z par celle du fragment. C'est toujours possible, on peut remplacer l'octet de ''stencil'' dans le tampon de ''stencil'' par celui du fragment s'il passe le test. Mais pour les techniques de rendu plus complexes, c'est une autre opération qui est utilisée, comme incrémenter l'octet dans le tampon de ''stencil''. ===Les effets de brouillard=== Les '''effets de brouillard''' sont des effets graphiques assez intéressants. Ils sont nécessaires dans certains jeux vidéo pour l'ambiance (pensez à des jeux d'horreur comme Silent Hill), mais ils ont surtout été utilisés pour économiser des calculs. L'idée est de ne pas calculer les graphismes au-delà d'une certaine distance, sans que cela se voie. L'idée est d'avoir un ''view frustum'' limité : le plan limite au-delà duquel on ne voit pas les objets est assez proche de la caméra. Mais si le plan limite est trop proche, cela donnera une cassure inesthétique dans le rendu. Pour masquer cette cassure, les programmeurs ajoutaient un effet de brouillard. Les objets au-delà du plan limite étaient totalement dans le brouillard, puis ce brouillard se réduisait progressivement en se rapprochant de la caméra, avant de s'annuler à partir d'une certaine distance. Pour calculer le brouillard, on mélange la couleur finale du pixel avec une ''couleur de brouillard'', la couleur de brouillard étant pondérée par la profondeur. Au-delà d'une certaine distance, l'objet est intégralement dans le brouillard : le brouillard domine totalement la couleur du pixel. En dessous d'une certaine distance, le brouillard est à zéro. Entre les deux, la couleur du brouillard et de l'objet devront toutes les deux être prises en compte dans les calculs. La formule de calcul exacte varie beaucoup, elle est souvent linéaire ou exponentielle. Notons que ce calcul implique à la fois de l'''alpha blending'' mais aussi la coordonnée de profondeur, ce qui en fait que son implémentation dans les ROPs est l'idéal. Aussi, les premières cartes graphiques calculaient le brouillard dans les ROP, en fonction de la coordonnée de profondeur du fragment. De nos jours, il est calculé par les ''pixel shaders'' et les ROP n'incorporent plus de technique de brouillard spécialisée. Vu que les pixels shaders peuvent s'en charger, cela fait moins de circuits dans les ROPs pour un cout en performance mineur. Et ce d'autant plus que les effets de brouillard sont devenus assez rares de nos jours. Autant les émuler dans les pixels shaders que d'utiliser des circuits pour une fonction devenue anecdotique. ===Les autres fonctions des ROPs=== Les ROPs gèrent aussi des techniques de '''''dithering''''', qui permettent d'adoucir des images lorsqu'elles sont redimensionnées et stockées avec une précision plus faible que la précision de calcul. Les ROPS implémentent aussi des techniques utilisées sur les ''blitters'' des anciennes cartes d'affichage 2D, comme l'application d''''opérations logiques''' sur chaque pixel enregistré dans le framebuffer. Les opérations logiques en question peuvent prendre une à deux opérandes. Les opérandes sont soit un pixel lu dans le ''framebuffer'', soit un fragment envoyé au ROP. Les opérations logiques à une opérande peuvent inverser, mettre à 0 ou à 1 le pixel dans le framebuffer, ou faire la même chose sur le fragment envoyé en opérande. Les opérations à deux opérandes lisent un pixel dans le framebuffer, et font un ET/OU/XOR avec le fragment opérande (une des deux opérandes peut être inversée). Elles sont utilisées pour faire du traitement d'image ou du rendu 2D, rarement pour du rendu 3D. Les ROPs gèrent aussi des '''masques d'écritures''', qui permettent de décider si un pixel doit être écrit ou non en mémoire. Il est possible d'inhiber certaines écritures dans le tampon de profondeur ou le tampon de couleur, éventuellement le tampon de stencil. Inhiber la mise à jour d'un pixel dans le tampon de profondeur est utile pour gérer la transparence. Si un pixel est transparent, même partiellement, il ne faut pas mettre à jour le tampon de profondeur, et cela peut être géré par ce système de masquage. Les masquages des couleurs permettent de ne modifier qu'une seule composante R/G/B au lieu de modifier les trois en même temps, pour faire certains effets visuels. ==L'architecture matérielle d'un ROP== Les ROP contiennent des circuits pour gérer la profondeur des fragments. Il effectuent un test de profondeur, à savoir que les fragments correspondant à un même pixel sont comparés pour savoir lequel est devant l'autre. Ils contiennent aussi des circuits pour gérer la transparence des fragments. Le ROP gère aussi l'antialiasing, de concert avec l'unité de rastérisation. D'autres fonctionnalités annexes sont parfois implémentées dans les ROP. Par exemple, les vielles cartes graphiques implémentaient les effets de brouillards dans les ROPs. Le tout est suivi d'une unité qui enregistre le résultat final en mémoire, où masques et opérations logiques sont appliqués. Les différentes opérations du ROP doivent se faire dans un certain ordre. Par exemple, gérer la transparence demande que les calculs de profondeur se fassent globalement après ou pendant l'''alpha blending''. Ou encore, les masques et opérations logiques se font à la toute fin du rendu. L'ordre des opérations est censé être le suivant : test ''alpha'', test du ''stencil'', test de profondeur, ''alpha blending''. Du moins, la carte graphique doit donner l'impression que c'est le cas. Elle peut optimiser le tout en traitant le tampon de profondeur, de couleur et de ''stencil'' en même temps, mais donner les résultats adéquats. [[File:Render Output Pipeline-processor.png|centre|vignette|upright=2|Render Output Pipeline-processor]] [[File:GeForce 6800 Pixel blending.png|droite|thumb|R.O.P des GeForce 6800.]] Un ROP est typiquement organisé comme illustré ci-dessous et ci-contre. Il récupère les fragments calculés par les pixels shaders et/ou les unités de texture, via un circuit d'interconnexion spécialisé. Chaque ROP est connecté à toutes les unités de ''shader'', même si la connexion n'est pas forcément directe. Toute unité de ''shader'' peut envoyer des pixels à n'importe quel ROP. Les circuits d'interconnexion sont généralement des réseaux d'interconnexion de type ''crossbar'', comme illustré ci-contre (le premier rectangle rouge). Notons que les circuits de gestion de la profondeur et de la transparence sont séparés dans les schémas ci-contre et ci-dessous. Il s'agit là d'une commodité qui ne reflète pas forcément l'implémentation matérielle. Et si ces deux circuits sont séparés, ils communiquent entre eux, notamment pour gérer la profondeur des fragments transparents. ==Les optimisations intégrées aux ROPs== Le ROP effectue beaucoup de lectures et écritures en mémoire vidéo. Or, la bande passante mémoire est limitée, ce qui fait que le ROP est un goulot d'étranglement assez important pour le rendu 3D. Heureusement, de nombreuses optimisations permettent d'optimiser le tout. Elles agissent sur la lecture du tampon de profondeur, mais aussi sur le ''framebuffer''. ===Le ''fast clear'' du ''framebuffer''=== Une première optimisation porte sur le ''framebuffer''. Le ''framebuffer''est souvent réutilisé d'une image sur l'autre. Quand une image a été envoyée à l'écran, le ''framebuffer'' est remis à zéro pour accueillir une nouvelle image. Et ce avec ou sans ''double buffering''. La mise à zéro est censée se faire en remettant réellement le ''framebuffer'' à zéro, en écrivant des 0 pour chaque pixel du ''framebuffer''. Mais il y a moyen de s'en passer. Pour cela, l'idée est que le ''framebuffer'' est découpé en ''tiles'', des carrés de 4, 8, 16 pixels de côté. Les ''tiles'' ont généralement la même taille que les ''tiles'' utilisées pour la rastérisation, mais passons sur ce détail. L'idée est de mémoriser, pour chaque ''tile'', si elle est mise à 0 ou non. Il suffit de cela d'un seul bit par ''tile'', appelé le bit RESET. L'ensemble des bits RESET est mémorisé dans une petite mémoire SRAM, intégrée aux ROPs. Lorsqu'on souhaite remettre à zéro le ''framebuffer'', il suffit de mettre à 0 tous les bits RESET dans cette SRAM, pas besoin d’accéder à la mémoire vidéo. Avant toute lecture dans le ''framebuffer'', le ROP lit cette SRAM pour vérifier si la ''tile'' en question a été remise à 0. Si ce n'est pas le cas, il lit le pixel voulu depuis le ''framebuffer''. Mais si c'est le cas, alors le ROP ne fait pas la lecture et fournit un pixel à zéro à la place, qui est utilisé pour l'''alpha blending'' ou autre. La moindre écriture dans une ''tile'' met le bit RESET à 0 : la ''tile'' entière est considérée comme non-remise à zéro, même si un seul pixel a été modifié dedans. Notons que l'usage d'une granularité par ''tile'' est un compromis. On peut ne peut pas utiliser un bit par pixel, car cela demanderait d'utiliser une SRAM énorme. De même, utiliser un seul bit pour tout le ''framebuffer'' ruinerait totalement l'optimisation : le ''framebuffer'' entier serait considéré comme non-RESET dès la première écriture d'un pixel dedans, on ne sauverait qu'un nombre trop limité d'accès mémoire. ===La z-compression=== La technique de '''z-compression''' compresse le tampon de profondeur. Plus précisément, elle découpe le tampon de profondeur en ''tiles'', en blocs carrés, qui sont compressés séparément les uns des autres. La taille des ''tiles'' est souvent la même que celle utilisée par le rastériseur pour la rastérisation grossière. Par exemple, la ''z-compression'' des cartes graphiques ATI radeon 9800, découpait le tampon de profondeur en ''tiles'' de 8 * 8 fragments, et les encodait avec un algorithme nommé DDPCM (''Differential differential pulse code modulation''). Précisons que cette compression ne change pas la taille occupée par le tampon de profondeur, mais seulement la quantité de données lue/écrite. La raison est que les ''tiles'' doivent avoir une place fixe en mémoire. Par exemple, si une ''tile'' non-compressée prend 64 octets, on trouvera une ''tile'' tous les 64 octets en mémoire vidéo, afin de simplifier les calculs d'adresse, afin que le ROP sache facilement où se trouve la ''tile'' à lire/écrire. Avec une vraie compression, les ''tiles'' se trouveraient à des endroits très variables d'une image à l'autre. Par contre, la z-compression réduit la quantité de données écrite dans le tampon de profondeur. Par exemple, au lieu d'écrire une ''tile'' non-compressée de 64 octets, on écrira une ''tile'' de seulement 6 octets, les 58 octets restants étant pas lus ou écrits. On obtient un gain en performance, pas en mémoire. [[File:AMD HyperZ.svg|centre|vignette|upright=2|AMD HyperZ]] Le format de compression ajoute un bit par ''tile'', qui indique si elle est compressée ou non. Le bit qui indique si la ''tile'' est compressée permet de laisser certaines ''tiles'' non-compressés, dans le cas où la compression ne permet pas de gagner de la place. La compression ajoute souvent un second bit, qui indique si la ''tile'' est à zéro ou non, sur le même modèle que pour le ''framebuffer''. Il accélère la remise à zéro du tampon de profondeur. Au lieu de réellement remettre tout le tampon de profondeur à 0, il suffit de réécrire un bit par ''tile''. Le gain en nombre d'accès mémoire peut se révéler assez impressionnant. Les deux bits en question peuvent être placés à deux endroits différents. La première solution serait d'utiliser une portion de la mémoire vidéo, mais cela demanderait de faire deux lectures par accès au tampon de profondeur. La vraie solution est d'utiliser une SRAM reliée aux ROPs, qui est assez grande pour mémoriser tout le tampon de profondeur, du moins avec deux bits par ''tile''. ===Le cache de profondeur=== Une optimisation complémentaire ajoute une ou plusieurs mémoires caches dans le ROP, dans le circuit de profondeur. Ce '''cache de profondeur''' stocke des portions du tampon de profondeur qui ont été lues ou écrite récemment. Comme cela, pas besoin de les recharger plusieurs fois : on charge un bloc une fois pour toutes, et on le conserve pour gérer les fragments qui suivent. Sur certaines cartes graphiques, les données dans le cache de profondeur sont stockées sous forme compressées dans le cache de profondeur, là encore pour augmenter la taille effective du cache. D'autres cartes graphiques ont un cache qui stocke des données décompressées dans le cache de profondeur. Tout est question de compromis entre accès rapide au cache et augmentation de la taille du cache. Il faut savoir que les autres unités de la carte graphique peuvent lire le tampon de profondeur, en théorie. Cela peut servir pour certaines techniques de rendu, comme pour le ''shadowmapping''. De ce fait, il arrive que le cache de profondeur contienne des données qui sont copiées dans d'autres caches, comme les caches des processeurs de shaders. Le cache de profondeur n'est pas gardé cohérent avec les autres caches du GPU, ce qui signifie que les écritures dans le cache de profondeur ne sont pas propagées dans les autres caches du GPU. Si on modifie des données dans ce cache, les autres caches qui ont une copie de ces données auront une version périmée de la donnée. C'est souvent un problème, sauf dans le cas du cache de profondeur, pour lequel ce n'est pas nécessaire. Cela évite d'implémenter des techniques de cohérence des caches couteuses en circuits et en performance, alors qu'elles n'auraient pas d'intérêt dans ce cas précis. ===Le ''z-fast pass''=== Le ''z-fast pass'' améliore la performance des '''prépasses z''', une technique utilisée par de nombreux moteurs de jeux vidéo. L'idée est que le moteur de jeu effectue plusieurs passes, chacune faisant un truc précis, la prépasse z étant l'une de ces passes. Lors d'une prépasse z, le moteur de jeu calcule la scène 3D, rastérise l'image, et remplit le tampon de profondeur uniquement. Il le place pas de textures, ne calcule pas de pixels shaders, il se préoccupe uniquement des coordonnées de profondeur des pixels. Au final, le rendu ne donne que le tampon de profondeur, qui est utilisé par les passes suivantes. L'utilité est très variable, mais il y a deux raisons pour effectuer une prépasse z : la performance, mais aussi certains effets graphiques. Par exemple, les effets d'occlusion ambiante "''screen space''" utilisent le tampon de profondeur pour faire leur travail. Il en est de même pour les ''shadowmaps'', qui effectuent une prépasse z par ombre à afficher. Une autre utilisation est que cela permet d'utiliser élimination des pixels cachés très performante. On effectue une prépasse z pour calculer le tampon de profondeur final, qui est ensuite utilisé par les passes suivantes pour éliminer les pixels cachés. Ainsi, les pixels cachés ne sont pas texturés et pixel shadés, avec certitude. Toujours est-il qu'une prépasse z utilise les ROP "à moitié", dans le sens où seul le tampon de profondeur est utilisé, par la gestion des couleurs. Mais il se trouve que les circuits qui servent pour l''alpha blending'' peuvent être réutilisés pour faire les comparaisons de profondeur ! Le résultat est que les ROP peuvent fonctionner à double vitesse lors d'une prépasse z ! Cela demande cependant de concevoir les circuits du ROP pour en profiter. L'optimisation est parfois appelée le '''''z-fast pass'''''. Tous les GPU depuis la Geforce FX en sont capables. Il y a cependant quelques contraintes. Premièrement, le ROP doit être configuré de manière à n’accéder qu'au tampon de profondeur, ils ne doivent pas dessiner dans le ''framebuffer''. L'''alpha blending'' doit être désactivé, de même que l'alpha-test. D'autres contraintes supplémentaires sont parfois présentes, surtout sur les vieux GPUs. Par exemple, l'antialiasing doit être désactivé lors de la prépasse z. Et mine de rien, cela ne marche que pour les prépasses z pures. Par exemple, certaines techniques de rendu différé augmentent la prépasse z pour que celle-ci ne calcule pas que le tampon de profondeur, mais aussi d'autres informations comme les normales : elles ne profitent pas de cette optimisation. {{NavChapitre | book=Les cartes graphiques | prev=Les unités de texture | prevText=Les unités de texture | next=Le support matériel du lancer de rayons | nextText=Le support matériel du lancer de rayons }}{{autocat}} 7jod7w3a2vlqaaohk82xs0jodcoqtsu Les cartes graphiques/Le rasterizeur 0 67396 763465 761677 2026-04-11T16:01:43Z Mewtow 31375 /* La parallélisation de la rastérisation */ 763465 wikitext text/x-wiki [[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.]] À ce stade du pipeline, les sommets ont été regroupés en primitives. Vient alors l'étape de '''rasterization''', durant laquelle chaque pixel de l'écran se voit attribuer un ou plusieurs triangle(s). Cela signifie que sur le pixel en question, c'est le triangle attribué au pixel qui s'affichera. Pour mieux comprendre quels triangles sont associés à tel pixel, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associé au pixel correspondant. Il est rare qu'on ne trouve qu'un seul triangle sur la trajectoire d'un pixel : c'est notamment le cas quand plusieurs objets sont l'un derrière l'autre. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. Et n'allez pas croire que seul l'objet situé devant les autres détermine à lui seul la couleur du pixel : n'oubliez pas que certains objets sont transparents ! Même avec des objets opaques, on doit calculer un pseudo-pixel pour chaque triangle. Les pseudo-pixels en question sont appelés des '''fragments'''. Pour chaque pixel, il y a un fragment par triangle situé "sur ce pixel". Les fragments attribués à un même pixel sont combinés pour obtenir la couleur finale de ce pixel. Mais cela s'effectuera assez loin dans le pipeline graphique, et nous reviendrons dessus en temps voulu. L'étape de rastérisation contient plusieurs étapes distinctes, que nous allons voir dans ce chapitre. C'est lors de cette phase que la perspective est gérée, en fonction de la position de la caméra. Diverses opérations de ''clipping'' et de ''culling'', qui éliminent les triangles non-visibles à l'écran, se font aussi après la rasterization ou pendant. La quasi-totalité des cartes graphiques récentes incorporent un circuit de zastérization, appelé le '''rasterizeur'''. Les seules exceptions sont les cartes graphiques très anciennes, mais aussi certaines cartes graphiques intégrées des processeurs Intel datant des années 2010. De nos jours, aucune carte graphique, même bas de gamme ou intégrée, n'est dans ce cas. ==Le ''viewport clipping''== [[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]] A la suite l'assemblage des primitives, une phase de ''view frustum culling'' élimine tout ce qui n'est pas dans le champ de vision de la caméra. Pour rappel, le ''view frustrum'' est délimité par six plans : quatre pour les bords de l'écran, un ''near plane'' pour les objets trop proches, un ''far plane'' pour les objets lointains. Le ''view frustum culling'' élimine tout ce qui est situé en-dehors du ''view frustum'', du champ de vision de la caméra. La détermination de ce qui est dans le champ de vision se fait triangle par triangle. Un triangle est soit totalement dans le champ de vision, soit totalement en dehors, soit partiellement dans le champ de vision. Les deux premiers cas sont triviaux à gérer : le triangle est envoyé à la rastérisation s'il est totalement visible, éliminé s'il est totalement invisible. Les triangles partiellement visibles sont un cas à part. Deux grandes solutions sont possibles pour les triangles partiellement visibles. La première découpe ces triangles en plusieurs triangles, tous présents intégralement dans le champ de vision. Mais il n'est pas certain que les GPU modernes aient des circuits dédiés à cette opération, aussi nous laissons le sujet de côté. La seconde est simplement de déléguer le travail au rastériseur et c'est cette solution qui est souvent utilisée sur les GPU modernes. Nous en reparlerons donc dans la section sur le rastériseur. [[File:Cube clipping.svg|centre|vignette|upright=2|''Clipping''/''View frustum culling'' dans le cadre d'un écran de forme carrée (en gris).]] ===Le ''near'' et ''far plane clipping'' === La gestion des ''near'' et ''far plane'' est conceptuellement la plus simple, car elle ne se préoccupe de la coordonnée de profondeur. La profondeur d'un sommet est comparé à deux seuils : un pour le ''near plane'', un autre pour le ''far plane''. Si sa profondeur est entre les deux seuil, le sommet est visible. La comparaison est faite pour les trois sommet d'un triangle et une décision est prise. Si les trois sommets sont visible, le triangle est intégralement visible, on l'envoie au rastériseur. Si les trois sommets sont invisibles, le triangle n'est pas dans le champ de vision, on l'élimine. Si une partie seulement des sommets est visible, alors le triangle est partiellement visible, on délègue le cas au rastériseur. De rares applications demandent de ne pas tenir compte du ''near'' et du ''far plane''. Le champ de vision est alors une pyramide infinie, sans limite. Un exemple est celui des ombres volumétriques, autrefois utilisées dans quelques moteurs graphiques, le meilleur exemple étant celui de DOOM 3. L'extension OpenGL GL_EXT_depth_clamp permettait de se passer de ces deux plans, afin de rendre une pyramide parfaite. ===Le ''viewport clipping''=== La gestion du ''viewport'' est elle plus compliquée, mais suit une sorte de dérivé de l''''algorithme de Cohen–Sutherland''', adapté pour des triangles. Pour comprendre ce que fait cet algorithme, il faut savoir une chose importante. En sortie des étapes de transformation, chaque sommet a trois coordonnées x, y et z. L'étape de transformation de la caméra a centré ces coordonnées avec le centre du regard. Le milieu de l'écran est aux coordonnées 0,0, pour les coordonnées x,y. Les bords de l'écran correspondent donc à quatre plan, définis par leurs coordonnées x et y. [[File:View transform.svg|centre|vignette|upright=2|Résultat de l'étape de transformation de la caméra.]] Prenons un écran de résolution W * H, avec H pixels en hauteur et W en largeur. * Les bord verticaux de l'écran ont pour coordonnées respectives <math>y_{min}</math> et <math>y_{max}</math> * Les bord horizontaux de l'écran ont pour coordonnées respectives <math>x_{min}</math> et <math>x_{max}</math> {| |[[File:Cohen-Sutherland 3.png|vignette|upright=2|Bords horizontaux.]] |[[File:Cohen-Sutherland 4.png|vignette|upright=2|Bords verticaux.]] |} Le ''clipping'' demande donc simplement de comparer les coordonnées du sommet avec ces quatre coordonnées et de combiner les résultats. Si le pixel a une coordonnée y dans l'intervalle <math>y_{min} , y_{max}</math> et une coordonnée x dans l'intervalle <math>x_{min} , x_{max}</math>, alors il est dans le champ de vision. Sinon, le sommet est non-visible. Il y a donc quatre comparaisons à faire par sommet, dont les résultats sont codés chacun sur un bit. Les 4 bits sont combinés pour savoir si le sommet est dans le champ de vision (les 4 bits sont à 1), ou en-dehors. L valeur exacte des 4 bits de résultat donne la position du sommet à l'écran. Un simple ET logique entre les 4 bits dit s'il est totalement visible ou non. [[File:Cohen sutherland kody.svg|centre|vignette|upright=1.5|Résultat des 4 comparaison en fonction de la position à l'écran du sommet.]] Mais cela ne fonctionne que pour un sommet. Or, le ''clipping'' se fait en tenant compte des trois sommets d'un triangle. Reprenons : l'étape précédente a testé chaque sommet, ce qui a donné un résultat codé sur un bit, pour chaque sommet. Le bit en question dit si le sommet est dans le champ de vision ou non. Tester les trois sommets demande de combiner ces trois bits pour obtenir le résultat final. Si les trois bits sont à 1, les trois sommets sont tous visibles, le triangle est totalement visible, il passe à la rastérisation. Si les trois bits sont à zéro, alors le triangle a ses trois sommets en dehors du champ de vision, le triangle est invisible, tout calcul est abandonné. Dans tout autre cas, une partie des sommets est dans le champ de vision et pas l'autre, le triangle est partiellement visible. Les triangles partiellement visibles sont traités à part, mais la grosse majorité est envoyée à la rastérisation. ==Le ''scanline converter'', ou rastériseur proprement dit== [[File:Pixels covered by a triangle.png|vignette|upright=1|Pixels couverts par un triangle.]] Une fois tous les triangles non-visibles éliminés, la carte graphique attribue les primitives restantes à des pixels : c'est l'étape de rastérisation proprement dite. Pour une carte graphique, seuls des triangles sont utilisés, ce qui fait qu'elle est appelée étape de ''Triangle Setup''. L'idée est que pour chaque triangle, la rastérisation détermine quels pixels sont dans le triangle. Le tout est illustré ci-contre, pour un triangle. ===Les règles de rastérisation=== Avant toute chose, il faut préciser ce qu'on entend quand on dit qu'un pixel est dans un triangle. Le cas où un pixel n'est pas exactement dans un triangle est assez compliqué à gérer. En effet, il arrive qu'un bord de triangle traverse un pixel à l'écran. En clair, si vous prenez le petit carré que représente votre pixel à l'écran, le bord du triangle passe dedans. La solution la plus simple considère que le pixel n'est pas un petit carré de couleur unie, mais est un point situé à une coordonnée x,y. Le point est placé à une coordonnée bien précise dans le carré, et c'est cette coordonnée bien précise qui est utilisée pour la rastérisation. le point est généralement pris au le centre du pixel, au centre du carré. C'est la solution choisie par les API graphiques modernes, comme Direct X. Mais il existe d'autres méthodes alternatives. Par exemple, il est possible de choisir un des coins du pixel ! Le coin inférieur gauche, par exemple. Tout cela pour dire que la scène 3D est ''échantillonnée'' par la rastérisation. [[File:Rectangle raster offset.svg|centre|vignette|upright=2.|Différence entre rastérisation au centre du pixel et sur le coin inférieur gauche.]] Il faut aussi tenir compte du cas le centre d'un fragment/pixel est pile entre deux triangles. Par exemple, si le centre du pixel est sur un segment, il faut contact avec deux triangles. Idem si le centre d'un pixel/fragment est sur un sommet, donc un point où plusieurs triangles font contact. Dans ce cas, il faut attribuer le pixel/fragment à un seul triangle, pas à plusieurs. Là encore, les API graphiques définissent des règles de rastérisation assez complexe, que le hardware doit respecter. [[File:Top-left triangle rasterization rule.gif|centre|vignette|upright=2.5|Top-left triangle rasterization rule]] Une méthode alternative calcule quelle proportion du pixel est dans le triangle. Par exemple, si le bord du triangle coupe le pixel en deux parties égales, alors 50% du pixel est dans le triangle, 50% ne l'est pas. Ces proportions sont transmises à la suite du pipeline pour gérer les calculs de couleur finaux, dans les ROPs. L'avantage est que si un pixel carré est partiellement couvert par plusieurs triangles, chacun attribuant une couleur au pixel, la couleur finale du pixel sera une moyenne pondérée de ces couleurs. Un tel système est une forme spécifique d'antialiasing appelée '''''alpha antialiasing'''''. ===La génération des pixels à rastériser=== Le rastériseur prend en entrée un triangle et détermine quels sont les pixels qui sont dans ce triangle. La méthode naïve regarde, pour chaque pixel de l'écran, s'il est dans le triangle. En logiciel, la manière la plus simple est d'utiliser une boucle qui traite l'écran pixel par pixel. On commence par le pixel tout en haut à gauche de l’écran, et on balaye l'écran pixel par pixel, ligne par ligne. Pour chaque pixel, on détermine s'il est dans le triangle ou en-dehors. S'il est dedans, on effectue ensuite des opérations d'interpolation pour calculer sa coordonnée de profondeur, sa couleur, ses coordonnées de texture, ses normales, etc. Dans sa version la plus naïve, tous les pixels de l'écran sont testés pour chaque triangle. Mais si le triangle est assez petit, une grande quantité de pixels seront testés inutilement. Diverses optimisations ont été inventées pour limiter le nombre de pixels à tester. L'optimisation la plus importante consiste à déterminer le plus petit rectangle possible qui contient le triangle, appelé la '''''bounding boxe'''''. L'idée est de ne tester que les pixels dans ce rectangle. Non seulement on calcule moins de pixels, mais les pixels calculés sont assez proches les uns des autres, bien que ce ne soit pas parfait. L'économie de calcul est assez large, surtout pour les petits triangles. Pour calculer la ''bounding boxe'', il suffit de prendre les trois sommets et de garder les plus grandes et plus petites coordonnées x et y. Les quatre coordonnées sont celles de la ''bounding boxe''. [[File:Smallest rectangle traversal.jpg|centre|vignette|Smallest rectangle traversal]] : Notez qu'on utilise un rectangle car la rastérisation se fait ligne par ligne, pixel par pixel. Il faut donc que le tout soit aligné sur des lignes horizontales. L'algorithme naïf peut être implémenté en matériel. La rastérisation se fait alors pixel par pixel, assez simplement. Pour générer les pixels, rien de plus simple : quelques compteurs suffisent ! Une implémentation sans ''bounding boxe'' a juste besoin d'un compteur de ligne et d'un compteur de colonne. Et de circuits pour transformer ces coordonnées entières en coordonnées flottantes, car les coordonnées des sommets à l'écran sont des flottants. Une implémentation avec a besoin d'ajouter des comparateurs et des comparateurs. La complexité réelle se trouve dans les circuits d'interpolation et pour tester si le pixel est dans le triangle. Il est même possible d'adapter le tout de manière à générer plusieurs pixels à la fois. Les autres méthodes de rastérisation fonctionnent sur le même principe : elles génèrent des pixels, testent si ceux-ci sont dans le triangle, puis effectuent des opérations d'interpolation. La seule différence est qu'elles ne testent pas tous les pixels de l'écran, mais tentent de se limiter à un sous-ensemble des pixels, qui ont de bonnes chances d'être dans le triangle. L'usage de ''bounding boxe'' est repris dans certains algorithmes qui vont suivre. [[File:Scan-line algorithm.svg|vignette|Rendu en Scan-line.]] Toujours est-il qu'avec ce qu'on vient de voir, la rastérisation se fait pixel par pixel. Mais il y a des méthodes alternatives. L'une d'entre elle traite l'écran ligne par ligne avec un algorithme de type '''rendu par scanlines''' (''scanline rendering''). Malheureusement, le rendu par scanline n'est pas du tout adapté pour une implémentation en matériel. Une des raisons est qu'idéalement, le rendu doit se faire par carrés de 2 pixels de côté, pour des raisons qu'on abordera dans le chapitre sur les textures. Il existe néanmoins quelques consoles de jeux qui ont implémenté cet algorithme en matériel. Un bon exemple est la console Nintendo 3DS et ses dérivés, qui utilisaient ce genre de rastérisation. Mais la quasi-totalité du matériel récent utilise une autre méthode de rastérisation, plus compatible avec des circuits et plus facilement parallélisable. ===Le test de couverture et les équations de droite=== Nous venons de voir comment la carte graphique génère les pixels/fragments à tester/rastériser. Il s'agit là de la première étape de la rastérisation. Elle est suivie, pour chaque pixel, par un test qui vérifie si le pixel/fragment est dans le triangle. Le test en question est appelé un '''test de couverture'''. C'est lors de cette étape que les règles de rastérisation vues plus haut sont prises en compte. Cette seconde étape est prise en charge par une ou plusieurs unités de rastérisation, chacune testant un pixel contre un triangle. Sur le matériel moderne, ce test utilise des équations de droite ! J'explique. Par définition, un triangle est une portion du plan délimitée par trois droites, chaque droite passant par un côté. Et chaque droite coupe le plan en deux parties : une à gauche de la droite, une autre à sa droite. Un triangle est définit par l'ensemble des points qui sont du bon côté de chaque droite. Par exemple, si je prend un triangle délimité par trois droites d1, d2 et d3, les points de ce triangle sont ceux qui sont situé à droite de d1, à gauche de d2 et à droite de d3. La rastérisation matérielle profite de cette observation pour déterminer si un pixel appartient à un triangle. L'idée est de calculer si un pixel est du bon côté de chaque droite, et de combiner les trois résultats pour prendre une décision. Pour chaque droite, on crée une '''fonction de contours''', qui indique de quel côté de la droite se situe le pixel. La fonction de contours va, pour chaque point sur l'image, renvoyer un nombre entier : * zéro si le point est placé sur la droite ; * un nombre négatif si le point est placé du mauvais côté de la droite ; * un nombre positif si le point est placé du bon côté. [[File:Rasterizing triangle edge.svg|centre|vignette|upright=1.5|Fonction de contours : le bleu est hors triangle, le rose dedans, un point est en-dehors de l'écran.]] Comment calculer cette fonction ? Tout d'abord, nous allons dire que le point que nous voulons tester a pour coordonnées <math>(x,y)</math> sur l'écran. La droite passe quant à elle par deux sommets : le premier de coordonnées <math>(X_1,Y_1)</math> et l'autre de coordonnées <math>(X_2,Y_2)</math>. La fonction est alors égale à : : <math>f(x,y) = (x-X_1) \times (Y_2-Y_1) - (y-Y_1) \times (X_2-X_1)</math> Pour savoir si un pixel appartient à un triangle, il suffit de tester le résultat des trois fonctions de contours, une pour chaque droite. A l'intérieur du triangle, les trois fonctions (une par côté) donneront un résultat positif. A l'extérieur, une des trois fonctions donnera un résultat négatif. ===Le ''back face culling''=== Après le ''viewport clipping'', une étape de ''back-face culling'' élimine les primitives qui tournent le dos à la caméra. Ces primitives appartiennent aux faces à l'arrière d'un objet opaque, qui sont cachées par l'avant. Elles ne doivent donc pas être rendues et sont donc éliminées du rendu. Le ''back face culling'' réutilise les résultats des fonctions de contours pour déterminer si le triangle est visible ou non. Le ''back face culling'' calcule, pour un triangle, la normale de la surface. Pour rappel, la normale est un vecteur qui est perpendiculaire à la surface, ici le triangle. L'idée est de regarder l'orientation de cette normale par rapport à la ligne de vue, un vecteur qui part de la caméra et atterit sur le triangle. Si la normale est perpendiculaire par rapport à cette ligne de vue, alors la surface est pile à l'horizontale et est presque invisible. Si la normale est penchée vers l'arrière, le triangle est là aussi invisible. Mais si elle est penchée vers l'avant, le triangle est visible. Le calcul de l'angle avec la ligne de vue est un simple produit scalaire entre les deux vecteurs (normale, ligne de vue). Et la normale est calculée à partir des fonctions de contours. ===La rastérisation sur les toutes premières cartes graphiques=== Au tout début du rendu 3D, dans les années 70-80, les cartes graphiques ne géraient pas que des triangles, voire n'en gérait pas du tout ! Le rendu 3D de l'époque gérait des polygones arbitraires, qui étaient découpés en polygones plus simples lors de la rastérisation. La rastérisation se faisait alors en deux étapes : on découpait les polygones en polygones de base, qui eux-même étaient rastérisés. C'était possible car les cartes graphiques de l'époque implémentaient le rastériseur avec des processeurs couplés à un ''firmware''/microcode dédié, pas des circuits fixes. Les polygones de base étaient parfois des triangles, mais aussi des trapézoïdes. Chaque carte graphique faisait à sa sauce, l'époque, il n'y avait pas de standardisation. Mais un cas très intéressant à étudier est celui des "trapézoïdes plats", à savoir des trapézoïdes alignés à l'horizontale. En clair, les segments du haut et du bas étaient à l'horizontale, alignés avec l'axe x. Intuitivement, un trapézoïde est définit par 4 fonctions de contours, quatre droites. Mais le fait d'aligner le haut et le bas à l'horizontale simplifie grandement le tout : deux droites disparaissent ! Les droites pour le segment du haut et du bas sont être remplacées par les coordonnées y du segment du haut et du bas. Il ne reste que les segments penchés, sur les bords. Au final : deux droites, deux coordonnées y. Le tout est plus simple que ce qu'on a avec les triangles, qui demandent trois droites. Un autre avantage est que la rastérisation est plus rapide si on utilise une rastérisation par ''scanline''. A l'écran, le trapézoïde plat est composé en empilant des lignes de pixels les unes sur les autres. Les lignes changent juste de longueur et de position à chaque ''scanline''. Le rastériseur a juste à calculer le point de départ à chaque ligne, et la longueur. Quelques compteurs font le reste pour dessiner le trapézoïde ligne par ligne, pixel par pixel. Le tout se mariait particulièrement bien avec le placage de texture direct, de type ''forward''. ==Les circuits d'interpolation== Une fois l'étape de ''triangle setup'' terminée, on sait donc quels sont les pixels situés à l'intérieur d'un triangle donné. Reste que ces pixels, il faut les remplir. Pour le moment, les seules informations que nous avons sont pour les sommets. On connait leur couleur, leur coordonnée de profondeur, leurs coordonnées de texture, et éventuellement d'autres informations. Mais les pixels/fragments à l'intérieur du triangle, ou sur ses bords, sont aussi censés avoir une couleur, des coordonnées de texture, et une coordonnée de profondeur. Reste à les calculer. [[File:Three geometry vertex computer graphics.png|vignette|Exemple d'interpolation de la couleur dans un triangle.]] Pour les pixels situés exactement sur les sommets, on peut reprendre la coordonnée de texture et la profondeur du sommet associé. Mais pour les autres pixels, nous sommes obligés d'extrapoler les coordonnées et la profondeur à partir des données situées aux sommets. C'est le rôle de l'étape d''''interpolation''', qui calcule les informations des pixels qui ne sont pas pile-poil sur un sommet. Par exemple, si j'ai un sommet vert, un sommet rouge, et un sommet bleu, le triangle résultant doit être colorié comme indiqué dans le schéma de droite. ===L'interpolation dans un triangle : les coordonnées barycentriques=== [[File:Linear interpolation in triangle.png|thumb|Coordonnées barycentriques.]] Pour interpoler une valeur dans un triangle, il est possible d'utiliser ce qui s'appelle les '''coordonnées barycentriques'''. Elles sont au nombre de trois coordonnées et sont notées u, v et w. Pour les déterminer, nous allons devoir relier le fragment aux trois autres sommets du triangle, ce qui découpe le triangle initial en trois triangles. Les coordonnées barycentriques sont proportionnelles aux aires de ces trois triangles. Par proportionnelles, il faut comprendre que les coordonnées barycentriques ne dépendent pas de la valeur absolue de l'aire des trois triangles. A la place, ces trois aires sont divisées par l'aire totale du triangle, et c'est ce rapport qui est utilisé pour calculer les coordonnée barycentriques. La carte graphique calcule ces trois coordonnées en commençant par normaliser l'aire du triangle. C'est à dire qu'elle fait en sorte que l'aire totale du triangle soit d'une unité d'aire, qu'elle fasse 1. Les aires des trois triangles sont alors calculées en proportion de l'aire totale, ce qui fait que leur valeur est comprise dans l'intervalle [0, 1]. Cela signifie que la somme de ces trois coordonnées vaut 1 : : <math>u + v + w = 1</math> Le calcul exact des coordonnées barycentriques peut se faire à partir des fonctions de contours. C'est pour cette raison qu'elles sont utilisées pour l'interpolation. Les trois coordonnées permettent de faire l'interpolation directement. Prenons l'exemple de l'interpolation de la couleur des trois sommets. Il suffit de multiplier la couleur d'un sommet par la coordonnée barycentrique associée, et de faire la somme de ces produits. Si l'on note <math>C_1</math>, <math>C_2</math>, et <math>C_3</math> les couleurs des trois sommets, la couleur d'un pixel vaut : : <math>\text{Couleur interpolée en x,y} = C_1 * u + C_2 * v + C_3 * w</math> Le seul problème est que l'interpolation avec des coordonnées barycentriques ne fonctionne tout simplement pas pour le rendu 3D ! Nous allons expliquer pourquoi dans ce qui suit. Mais sachez cependant que nous n'avons pas introduit ces coordonnées barycentriques pour rien. Les coordonnées barycentriques sont bien utilisées par la carte graphique, le rastériseur calcule ces coordonnées barycentriques avant de faire le moindre calcul d'interpolation. Simplement, elles ne sont pas utilisées naïvement pour faire l'interpolation avec la formule précédente. ===L'interpolation de la coordonnée de profondeur=== L'usage de coordonnées barycentriques ne fonctionne pas bien, car il ne tient pas compte de la perspective. Lors de l'étape de transformation et de rastérisation, la perspective utilisée donne un résultat idéal à l’œil. Sauf qu'elle altère les distances. Si je prend un bord de triangle, il correspond à un segment à l'écran. La coordonnée de profondeur ne varie pas linéairement sur ce segment. Or, l'usage de coordonnées barycentriques est par nature linéaire. Pour tenir compte de la perspective, il faut d'abord interpoler correctement la coordonnée de profondeur. La coordonnée de profondeur est interpolée avant tout le reste. La profondeur interpolée est en effet nécessaire pour interpoler les couleurs des sommets, les coordonnées de texture, et autres. Donc nous allons voir les deux méthodes pour interpoler la coordonnée de texture avant le reste. Il existe deux solutions pour interpoler la coordonnée de profondeur. La plus simple utilise malgré tout une '''interpolation linéaire''', aussi appelée interpolation affine. La coordonnée de profondeur est alors calculée avec les coordonnées barycentrique, avec la même formule d'interpolation que celle vue plus haut. La coordonnée de profondeur est alors incorrecte, ce qui se répercute sur le calcul des coordonnées de texture. : <math>z_{x,y} = z_1 * u + z_2 * v + z_3 * w</math>, avec <math>z_{x,y}</math> la profondeur interpolée au point x,y. Il est aussi possible d'utiliser une '''interpolation quadratique''', qui est bien plus proche d'une perspective correcte. Quelques ordinateur ont utilisé cette technique, notamment de vielles stations de travail des années 80, à une époque où une correction de perspective digne de ce nom était trop gourmande en circuits. Je la mentionne pour la complétude, elle n'a presque pas été utilisée dans les cartes graphiques anciennes comme modernes. Une autre solution utilise une '''interpolation tenant compte de la perspective'''. Sans rentrer dans les détails mathématiques du calcul de la perspective, l'idée est d'utiliser non pas la coordonnée de profondeur z, mais son inverse 1/z. En effet, cette valeur suit une évolution linéaire vu de l'écran, à savoir qu'un objet qui parait deux fois plus loin à l'écran sera situé à une distance de 2/z. Et c'est ce qui rend l'interpolation linéaire possible avec 1/z. Pour faire l'interpolation, on calcule 1/z pour les trois sommets, on effectue une interpolation de 1/z avec des coordonnées barycentriques, puis on inverse le résultat pour trouver le z voulu. L'interpolation se fait donc avec le calcul suivant : : <math>{1 \over z_{x,y} } = {u \over z_1} + {v \over z_2} + {w \over z_3}</math> Le GPU contient un circuit pour interpoler 1/z, et il est assez complexe si on en croit la formule précédente. Trois divisions, deux additions. Autant les additions sont tout sauf un problème, autant les divisions sont très gourmandes en circuits, particulièrement les divisions flottantes. Et ce sont en plus des opérations très lentes ! Il n'est pas étonnant que les rastériseurs soient implémentés en matériel sachant cela, et encore : on n'a pas vu les autres interpolations... La formule précédente permet de calculer z si besoin, en prenant l'inverse. Mais le truc est que l'inverse n'est pas utilisé très souvent. Il se trouve que les calculs dans la suite de ce chapitre font surtout usage de 1/z, pas de z directement. Aussi, les GPU se débrouillent pour utiliser le plus possible la valeur de 1/z, et non z directement. Par exemple, le tampon de profondeur ne mémorise pas la valeur de z pour chaque pixel ! A la place, il mémorise les valeurs de 1/z ! Et cela a de nombreux avantages, qui sont exploités par les circuits liés au tampon de profondeur. : Mieux encore, les cartes graphiques modernes utilisent en fait une formule plus compliquée, pour tenir compte du ''near plane'' et du ''far plane'', qui délimitent l'avant et l'arrière du ''view frustum''. La formule exacte est : :: <math>a \times {1 \over z} + b</math>, avec a et b deux constantes liées à la position du''near plane'' et du ''far plane''. ===L'interpolation tenant compte de la perspective=== Maintenant, voyons comment interpoler les autres valeurs aux sommets : couleur, coordonnées de textures, normales, etc. Nous allons prendre l'exemple de la couleur, pour plusieurs raisons. Premièrement, la couleur est un simple nombre (on met de côté son caractère RGB), ce qui simplifie les explications. Et la couleur est un paramètre plus intuitif qu'une normale, par exemple. Ensuite, le cas des coordonnées de texture sera vu à part, car je tiens à faire un aparté sur le placage de texture affine. Pour l'interpolation de la couleur, le calcul procède comme suit. L'idée est de multiplier chaque couleur par 1/z, z étant pris sur le sommet adéquat. On a alors les trois coordonnées suivantes : : <math>{C_1 \over z_1} , {C_2 \over z_2} , {C_3 \over z_3}</math> L'interpolation utilise ces coordonnées, au lieu des coordonnées barycentriques : : <math>C_{x,y} = u * {C_1 \over z_1} + u * {C_2 \over z_2} + u * {C_3 \over z_3}</math> Puis on multiplie ce résultat par z, la coordonnée de profondeur mesurée sur le pixel interpolé. : <math>\text{Couleur interpolée en x,y} = z_{x,y} \times C_{x,y} = z_{x,y} \times \left[ u * {C_1 \over z_1} + u * {C_2 \over z_2} + u * {C_3 \over z_3} \right]</math> Une formule équivalente à la précédente ne multiplie pas par z, mais divise par 1/z. La valeur de 1/z est facile à calculer, il suffit de l'interpoler comme expliqué plus haut. : <math>\text{Couleur interpolée en x,y} = \left[ u * {C_1 \over z_1} + u * {C_2 \over z_2} + u * {C_3 \over z_3} \right] \div {1 \over z}</math> Le matériel pour faire ces calculs est mine de rien assez gourmand en circuits. * La première étape utilise un circuit diviseur, ou équivalent. * La seconde étape utilise deux circuits : un pour calculer les coordonnées barycentriques, et un circuit d'interpolation proprement dit. * La troisième étape utilise un diviseur pour diviser par 1/z. Nous avons donc une formule pour faire une interpolation correcte niveau perspective, que l'on peut implémenter en circuit. Quelques circuits diviseurs, multiplieurs et additionneurs suffisent. Le bilan niveau circuits est : trois divisions pour la première étape, trois multiplications et deux additions pour la seconde étape, une multiplication pour la troisième étape. Une optimisation effectue chaque division de la première étape dans un circuit qui calcule l'inverse d'un nombre, et un circuit multiplieur. Le circuit qui calcule l'inverse d'un nombre peut être utilisé dans les calculs d'interpolation, pour faire le calcul 1/z. C'est donc une forme de redondance exploitable. ===L'interpolation des coordonnées de texture=== Lors de la rastérisation, chaque fragment se voit attribuer un triangle, et les coordonnées de texture qui vont avec. Si un pixel est situé pile sur un sommet, les coordonnées de texture de ce sommet sont attribuées au pixel. Si ce n'est pas le cas, les coordonnées de texture sont interpolées à partir des trois sommets du triangle rastérisé. Et cette interpolation peut ou non tenir compte de perspective, tenir compte de la coordonnée de profondeur. Le '''placage de texture affine''' interpole les coordonnées de texture sans tenir compte de la coordonnée de profondeur. Concrètement, on fait une moyenne pondérée des coordonnées de texture u et v des trois sommets pour obtenir les coordonnées de textures finales, sans prendre en compte la coordonnée de profondeur, avec des coordonnées barycentriques. Par contre, en faisant cela, la perspective n'est pas correctement rendue, comme illustré ci-dessous. Le '''placage de texture avec perspective correcte''' tient compte de la coordonnée de profondeur pour interpoler les coordonnées de texture. Mais pour cela, il faut que la coordonnée de profondeur soit interpolée avec une perspective correcte, c'est à dire en interpolant 1/z, pour ensuite inverser le tout. Plus précisément, il faut : * remplacer les coordonnées u,v,z (les deux coordonnées de texture u,v et la profondeur) par les coordonnées suivantes : u/z, v/z et 1/z ; * interpoler ces quantités avec des coordonnes barycentriques ; * de multiplier le tout par z pour obtenir le résultat final. En faisant cela, on s'assure que la perspective est rendue à la perfection. L'explication mathématique de pourquoi cette formule fonctionne est cependant assez compliquée... [[File:Perspective correct texture mapping.svg|centre|vignette|upright=2|Correction de perspective.]] L'interpolation affine était utilisée sur la console Playstation 1 de Sony, d'où des textures un peu bizarres sur cette console. D'autres consoles utilisaient l'interpolation affine, mais s'en sortaient mieux car elles utilisaient non pas des triangles, mais des ''quads'' (des rectangles). Avec des primitives rectangulaires, le résultat a l'air visuellement, meilleur, car l'interpolation donne un bon résultat pour ce qui va à l'horizontal, seule les objets à la verticale de la caméra donnant une perspective légèrement déformée. Tout cela est bien illustré ci-dessous. Cependant, l'interpolation est alors plus lourde en calculs, car elle demande d'interpoler quatre sommets au lieu de trois. Le cout en calculs n'est pas négligeable. [[File:Affine texture mapping tri vs quad.svg|centre|vignette|upright=2|Affine texture mapping tri vs quad]] ==Le circuit rastériseur au complet== Nous venons de voir que le rastériseur effectue beaucoup de choses. Il doit générer des pixels, vérifier s'il sont dans le triangle traité, puis effectuer plusieurs interpolations. Mais la complexité du rastériseur dépend de s'il utilise la correction de perspective ou non. Au tout début du rendu 3D, les rastériseurs se passaient souvent de correction de perspective. On parle des années 70-80, sur les stations de travail professionnelles. Mais les cartes accélératrices des PC ont directement utilisé la correction de perspective. Il a donc existé deux types de rastériseurs : avec et sans correction de perspective. Par exemple, la Playstation 1 a utilisé la rastérisation affine, sans correction de perspective. Et les deux types de rastériseurs n'ont pas la même conception. C'est ce qui explique pourquoi l'absence de correction de perspective était autrefois utilisée, mais a disparu. Pour cela, comparons un rastériseur avec et un sans correction de perspective. ===Les rastériseurs sans correction de perspective=== Un rastériseur sans correction de perspective est un peu plus simple que l'autre type. Il prend en entrée un triangle à rastériser, à savoir trois sommets. Il génère les pixels soit un par un, soit en parallèle. Nous allons supposer que des circuits génèrent les pixels un par un, pour tout simplifier. Une fois qu'il dispose du pixel à tester, et des trois ommets, il effectue la suite d'actions suivantes : * Il calcule les fonctions de contour et accepte ou rejette le triangle. * Il calcule les coordonnées barycentriques à partir des fonctions de contour. * Il effectue les interpolations à partir des coordonnées barycentriques. Les trois étapes sont réalisées par des circuits séparés. Le tout est illustré ci-dessous. [[File:Rasteriseur avec interpolation affine.png|centre|vignette|upright=2|Rasteriseur avec interpolation affine]] Les circuits de génération des pixels, de fonction de contours et de calcul des coordonnées barycentriques sont identiques avec et sans correction de perspective. Par contre, les circuits d'interpolation seront différents. Les circuits d'interpolation sont relativement simples sans correction de perspective. L'interpolation demande de faire quelques multiplications et additions flottantes ou entières. Quelques circuits de MAD (''Multiply And Accumulate'') sont tout indiqués. ===Les rastériseurs avec correction de perspective=== Le rastériseur avec correction de perspective fonctionne de manière similaire au rastériseur précédent. Comparé au rastériseur précédent, rien ne change pour la génération des pixels, des fonctions de contour et des coordonnées barycentriques. Par contre, tout change pour ce qui est de l'interpolation. L'implémentation exacte arrive à économiser de nombreux circuits diviseurs, pour les remplacer par des circuits plus simples. En effet, les valeurs <math>{1 \over z_1}</math>, <math>{1 \over z_2}</math> et <math>{1 \over z_3}</math> sont utilisés dans tous les calculs d'interpolation. Il est possible de calculer ces termes une fois pour toute, pour ensuite les utiliser dans les calculs d'interpolation. Les circuits d'interpolation prennent alors en entrée : les coordonnées barycentriques, les valeurs <math>{1 \over z_i}</math>, et les attributs à interpoler. Le résultat est que les circuits diviseurs sont remplacés par des circuits multiplieurs ou MAD (''Multiply And Accumulate''). Il s'agit d'une économie en circuits non-négligeable. [[File:Implémentation matérielle d'un rastériseur.png|centre|vignette|upright=2|Implémentation matérielle d'un rastériseur avec correction de perspective.]] Ce n'est pas évident dans le schéma précédent, mais la correction de perspective et les interpolations demandent d'effectuer des divisions flottantes, ce qui est très gourmand en calcul et en circuits. Les anciennes cartes graphiques préféraient l'interpolation affine, pour économiser ces circuits de division flottante. Le résultat était acceptable pour l'époque, en termes de ratio performance/qualité d'image. De nos jours, les GPU utilisent une perspective correcte, le placage de texture affine est définitivement abandonné. ===Les rastériseurs des GPU modernes=== Les GPU modernes vont encore plus loin : ils se passent de circuits d'interpolation. A la place, les calculs d'interpolation sont réalisés dans les pixel shaders. Le ''driver'' de la carte graphique ajoute de quoi faire l'interpolation quand il compile les pixels shaders, tout au début du shader. L'avantage est que cela économise pas mal de circuits, tout en ayant un cout en performance dérisoire. Le rastériseur se contente de générer les pixels, les coordonnées barycentriques, et éventuellement les valeurs <math>{1 \over z_i}</math>. Les opérations d'interpolation ne demandant que des opérations MAD (''Multiply And Accumulate''), les pixels shaders ont déjà le matériel pour cela. A vrai dire, même le calcul des valeurs <math>{1 \over z_i}</math> peut être fait dans les shaders, si le GPU supporte l'instruction RECP (Reciproqual, à savoir le calcul de 1/x). Et niveau performance, les processeurs de shaders sont tellement puissants de nos jours que leur rajouter quelques opérations d'interpolation basiques ne le fait presque rien. Un autre avantage est que cela permet de gérer divers types d'interpolation sans avoir de hardware dédié. Les GPU modernes supportent à la fois l'interpolation affine ou avec correction de perspective. Les anciens GPU devaient avoir des rastériseurs matériels capable de faire les deux types d'interpolation. Le gros du rastériseur était commun aux deux types d'interpolation, il y avait beaucoup de redondance, certes. Mais il fallait rajouter des circuits pour rendre le rastériseur configurable, capable de gérer les deux types d'interpolation. Avec une interpolation réalisée dans les shaders, pas besoin : on ne garde que ce qui est vraiment commun, à savoir la génération des coordonnées barycentriques et la génération des pixels/fragments. ==Les rastériseurs à deux niveaux/parallèles== Résumons rapidement ce qu'on vient de voir. Le rastériseur rastérise des triangles, c'est une évidence. Pour économiser des calculs, il détermine une ''bounding box'' rectangulaire, qui contient ce triangle. Tout cela n'est pas faux en soi, mais drastiquement incomplet. Les GPU, anciens comme modernes, utilisent en réalité une méthode de rastérisation améliorée, bien plus performante et plus adaptée à une implémentation en circuits. Les GPU utilisent précisément un algorithme dit de ''tiled traversal'', que nous allons expliquer dans ce qui suit. Elle a de nombreux avantages. Le premier est qu'elle permet de tester plusieurs pixels séparés en parallèle. En clair, la rastérisation d'un triangle ne se fait pas pixel par pixel. Elle se fait en réalité plusieurs pixels à la fois, le GPU teste et interpole plusieurs pixels en même temps. Le second avantage est que les pixels non-couverts par le triangle sont rejetés plus rapidement, en blocs. ===Le ''tiled traversal''=== Le principe consiste à découper le ''framebuffer'' en morceaux carrés de 4, 8, 16 pixels de côté, qui sont appelés des ''tiles''. Les ''tiles'' sont de taille et de position fixes. N'allez cependant pas croire qu'il s'agit des mêmes ''tiles'' que celles utilisées sur les architectures à ''tile''. Un point important est que les ''tiles'' ne sont pas forcément alignées sur la ''bounding box'', mais sont en réalité alignées avec l'écran, le ''framebuffer''. Mais c'est là un détail. L'idée est qu'un premier rastériseur découpe la ''bounding box'' en ''tiles'', puis détermine quelles ''tiles'' sont recouvertes par un triangle. La rastérisation se fait alors en deux temps, en deux niveaux. Un premier niveau découpe la ''bounding box'' en ''tiles'', et décide lesquelles sont couvertes par un triangle. Un second niveau rastérise les ''tile'' nécessaires, une par une. Le test de couverture d'une ''tile'' est assez simple : il suffit de tester les quatre sommets de la ''tile'' et de combiner les résultats. Un avantage de cette méthode se comprend bien avec l'exemple illustré dans le schéma ci-dessous, où un triangle recouvre trois ''tiles'', alors que sa ''bounding box'' en recouvre 4 ''tiles''. Si on se basait uniquement sur la ''bounding box'' pour éliminer les pixels/fragments inutiles, tous les pixels de la ''bounding box'' seraient testés. Avec ce système de ''tiles'', le GPU va remarquer qu'une ''tile'' ne recouvre pas le triangle, seules les trois autres seront traitées. En clair, seules 3 ''tiles'' sur 4 vont générer des pixels, la ''tile'' inutile n'est pas prise en compte. [[File:Tiled traversal.png|centre|vignette|Tiled traversal.]] L'avantage se manifeste surtout pour les gros triangles, couplés à des ''tiles'' assez petites. Plus la ''tile'' est petite et plus le triangle est grand, plus l'avantage sera important. L'avantage théorique est d'environ la moitié des pixels éliminés. Prenons un cas favorable : un triangle dont la base est horizontale. Dans ce cas, la ''bounding box'' rectangulaire a une largeur égale à la base du triangle et une hauteur égale à celle du triangle. La géométrie de collège nous dit que le triangle occupe la moitié de la ''bounding box'', pour ce qui est de l'aire. Donc, la moitié des pixels est recouverte. Le fait de découper l'aire en ''tile'' fait cependant passer sous les 1/2 de pixels éliminés. Mais le ''tiled traversal'' a un autre avantage, bien plus intéressant... ===La parallélisation de la rastérisation=== La ''tiled rasterisation'' permet d'exploiter l'amplification des pixels simplement. Avec la rastérisation à deux niveaux, il est possible de traiter tous les pixels/fragments d'une ''tile'' en parallèle, en une seule fois. Par exemple, pour une ''tile'' de 8x8 pixels, le rastériseur fin est capable de traiter les 64 pixels/fragments de la ''tile'' en une seule fois. Pour cela, les circuits vus précédemment, qui forment un rastériseur complet, sont dupliqués en autant d'exemplaires qu'il y a de pixels dans une ''tile''. Beaucoup de duplication de circuits, mais le gain en performance n'est pas négligeable. Tous les GPU actuels utilisent ce genre de duplication. Le ''tiled traversal'' a pour autre avantage qu'il se marie très bien avec la gestion des textures. En effet, les textures sont stockées en mémoire d’une manière particulière : elles sont découpées en carrés de quelques pixels de côté, et les carrés sont répartis dans la mémoire d'une manière assez spécifique. Les carrés des textures ont la même taille que les carrés de la rastérisation. Cela garantit que la rastérisation d'un carré de pixel a de bonnes chances de tomber sur un carré de texture, ce qui permet de profiter parfaitement du cache de texture. : Une solution complémentaire traite plusieurs ''tiles'' en parallèle. Le problème de cette optimisation est qu'elle n'est pas utile. Elle gaspille beaucoup de ressources pour les petits triangles, mais ne donne pas de gros gains pour les gros triangles. Notons que cette optimisation se marie particulièrement bien avec des circuits séparés pour les triangles et les pixels. Les anciennes cartes graphiques étaient dans ce cas. Elles avaient une unité pour la géométrie, et plusieurs unités pour les pixels/textures. Le rastériseur recevait les triangles provenant de l'unité géométrique, générait plusieurs pixels/fragments, qui étaient distribués sur les unités de texture/pixel. L'implémentation est plus compliquée quand on a des processeurs de shaders unifiés, mais laissons cela à plus tard. [[File:Architecture d'un GPU tenant compte de l'amplification des pixels.png|centre|vignette|upright=2.5|Architecture d'un GPU avec rastérisation parallèle (qui utilise l'amplification des pixels).]] ==L'élimination précoce des fragments cachés== La coordonnée de profondeur permet de savoir si un fragment doit être rendu ou non. Logiquement, si un objet est derrière un autre, il n'est pas visible et ses fragments n'ont pas à être calculés/rendus. Les concepteurs de cartes graphiques usuelles ont donc inventé des techniques d''''élimination précoce''' pour éliminer certains fragments dès qu'on connait leur coordonnée de profondeur, à savoir une fois l'étape de rastérisation/interpolation terminée. Ainsi, on est certain que le fragment en question n'est pas texturé et ne passe pas dans les pixels ''shaders'', ce qui est un gain en performance non-négligeable. Il faut certes prendre en compte la transparence des fragments qui sont devant, mais rien d'insurmontable. ===Les pixels shaders et le ''early-z''=== Mais ces techniques peuvent causer un rendu anormal quand un ''pixel shader'' modifie la coordonnée de profondeur ou de transparence d'un pixel. C'est rare, mais cela peut arriver. C'est pour cela que l’élimination des fragments invisibles est traditionnellement réalisé à la toute fin du pipeline graphique, dans les ROPs, juste avant d’enregistrer les pixels dans le ''framebuffer''. Pour éliminer tout problème, on doit activer ou désactiver l'élimination précoce des pixels suivant les besoins. Depuis DirectX 11, les APIs graphiques permettent de marquer certains ''shaders'' comme étant compatibles ou incompatibles avec l'élimination précoce. Avant, les drivers du GPU analysaient les shaders pour décider de faire le test de profondeur précoce. Rappelons que la carte graphique change régulièrement de ''shader'' à exécuter. Et il arrive qu'on passe d'un ''shader'' compatible avec l'élimination précoce à un ''shader'' incompatible ou inversement. Passer d'un ''shader'' qui est compatible avec l'élimination précoce à un qui ne l'est n'est pas un problème. Il suffit de désactiver l'unité d'élimination précoce lors du changement de ''shader''. Mais dans le cas inverse, quelques problèmes de synchronisation peuvent apparaitre. Il faut activer l'élimination précoce quand les pixels du nouveau ''shader'' sortent du circuit de rastérisation, ce qui n'est pas exactement le même temps que le changement de ''shader''. En effet, le ''shader'' précédent a encore des pixels qui traversent le pipeline et qui sont en cours de calcul dans les pixels ''shaders'' ou dans les ROP. Le processeur de commande doit donc faire attendre les processeurs de ''shader'' et quelques autres circuits. Typiquement, il faut attendre que la commande précédente se termine, avant d'en relancer une autre avec le nouveau ''shader''. Plus d'information dans cet article de blog : [https://therealmjp.github.io/posts/to-earlyz-or-not-to-earlyz/ To Early-Z, or Not To Early-Z]. ===L'élimination des pixel cachés hiérarchique=== Le ''tiled traversal'' est à l'origine de beaucoup d’optimisations, qu'on détaillera dans ce qui suit. Elle élimine beaucoup de pixels inutiles dans une ''bounding box'', elle permet de traiter tous les pixels d'une ''tile'' en parallèle, permet d'utiliser plusieurs rastériseurs. Mais une optimisation potentielle améliore l'élimination des pixels cachésen tenant compte des ''tiles''. Il y a alors une élimination des pixels cachés à deux niveaux : au niveau des ''tiles'', puis par pixel dans une ''tile''. On parle d''''élimination des pixels cachés précoce hiérarchique''', que nous allons abrévier en ''z-hiérarchique''. Il existe plusieurs techniques d'élimination précoce (''Early-Z''), qui peuvent être classées en deux catégories : le zmax, et le zmin. Dans les deux cas, la carte graphique vérifie, pour chaque pixel, s'il est affiché ou masqué. Parmi tous les pixels d'une ''tile'', il y en aura un dont la profondeur sera plus élevée ou plus petite que les autres. L'unité d'''Early-Z'' mémorise cette profondeur maximale ou minimale pour chaque ''tile''. Dans le cas du zmax, c'est la profondeur la plus grande qui est mémorisé, alors que le zmin mémorise la plus petite profondeur. Avce le z-max, le rastériseur mémorise la coordonnée z maximale de la ''tile'', celle la plus loin de l'écran. Lorsqu'un triangle est rendu dans une ''tile'', le rastériseur calcule la coordonnée de profondeur minimale, celle la plus proche de l'écran. Les deux coordonnées sont alors comparées : si la coordonnée minimale du triangle est plus grande que celle de la ''tile'', cela veut dire que tout le triangle est situé derrière la ''tile''. Donc qu'il est caché, on n'a pas à rendre ce triangle et faire la rastérisation, on abandonne tout. Pour calculer la valeur maximale des pixels de la tile, une solution calcule les coordonnées z des quatre coins de la ''tile'' et de prend la plus proche (la plus petite). Mais ces coordonnées doivent être interpolées, ce qui demande pas mal de calculs. Avec le zmin, on utilise la profondeur maximale des sommets du triangle et la profondeur minimale dans la tile. Si la profondeur du pixel à rendre est plus petite, cela veut dire que le pixel n'est pas caché et qu'il n'y a pas besoin d'effectuer de test de profondeur dans les ROPs. Il est parfaitement possible d'utiliser le zmax conjointement avec le zmin. On obtient alors des techniques hybrides, relativement puissantes. On pourrait citer l'exemple de l'adaptive tile depth filter. Les profondeurs de chaque ''tile'', maximale ou minimale, sont mémorisées dans une mémoire SRAM intégrée au rastériseur grossier, celui qui gère les ''tiles''. En théorie, on peut se débrouiller avec une seule SRAM pour tout le GPU, la SRAM étant partagée entre les différents rastériseurs grossiers, et les GPU modernes font sans douter ainsi. La SRAM en question a une taille limitée et diverses optimisations visent à réduire sa taille. Par exemple, au lieu d'utiliser des coordonnées de profondeur de 24 bits, d'usage dans les ROPs, la SRAM ne mémorise que les 16 bits de poids fort. Au pire, ce manque de précision réduira un peu l'efficacité de l'élimination des pixels cachés au niveau des ''tiles''. Et malgré cela, pour des grandes résolution, la SRAM est souvent trop petite malgré tout. L'avantage de cette SRAM est que le ''z-hiérarchique'' peut éliminer des pixels cachés sans avoir à lire le tampon de profondeur. Et lire le tampon de profondeur utilise beaucoup de bande passante en RAM vidéo. L'élimination des pixels cachés précoce, au niveau des pixels, demande de lire le tampon de profondeur. Mais celle au niveau des ''tiles'' n'accède pas à la mémoire vidéo, seulement à la SRAM mentionnée au paragraphe précédent. ==La parallélisation de la rastérisation== La performance de la rastérisation est particulièrement importante. Pour l'améliorer, la solution retenue est le parallélisme, à savoir faire des calculs indépendants en parallèle. Utiliser correctement le parallélisme est un classique quand on conçoit des processeurs, des GPU, ou n'importe quel circuit électronique. C'est un des grand avantage du matériel par rapport au logiciel : exécuter des calculs en parallèle est assez intuitif, simple, facile. Reste à concevoir des rastériseurs qui effectuent des calculs en parallèle. Pour cela, on a deux grandes solutions. La première utilise l'amplification des pixels, à savoir le fait qu'un triangle est "traduit" en plusieurs pixels/fragments. L'idée est de traiter un triangle à la fois, mais de générer plusieurs pixels/fragments en même temps, qui sont testés et interpolés séparément. La seconde solution traite plusieurs triangles séparés dans des rastériseurs séparés. La ''tiled rasterisation'' permet d'exploiter l'amplification des pixels simplement, ce qui est très utile. Une solution complémentaire rastérise plusieurs triangles en parallèle, dans des circuits rastériseurs séparés. Pour cela, les GPU actuels incorporent plusieurs rastériseurs. La raison à cela est assez simple : quand on a une centaine de processeurs de shaders à alimenter, il faut rastériser un grand nombre de pixels par seconde. Un seul circuit rastériseur n'est pas suffisant, on doit en utiliser plusieurs. Cependant, utiliser plusieurs rastériseurs pose pas mal de problèmes pratiques. Le premier est qu'il faut connecter les rastériseurs aux processeurs de shaders, et aux autres circuits de la carte graphique. La solution la plus performante utilise un réseau d'interconnexion qui relie les R rastériseurs aux P processeurs de shaders. L'idée est que n'importe quel processeur de shader peut envoyer des données à n'importe quel rastériseur et inversement. Mais un tel réseau est très compliqué à mettre en place. Il y a beaucoup de fils à câbler, sans compter que le transfert des données dans les fils consomme beaucoup de courant et chauffe beaucoup. Et il faut ajouter des circuits d'arbitrage pour décider quel triangle va dans quel rastériseur. En général, il y a moins de rastériseurs que de processeurs de shaders, c'est le cas sur les GPU modernes. La raison est une question d'amplification des pixels. Par exemple, supposons qu'un triangle couvre en moyenne 20 pixels. Dans ce cas, un rastériseur va produite environ 20 pixels par cycle, et alimentera 20 processeurs de ''pixel shaders''. En conséquence, il y a souvent moins de rastériseurs que processeurs de shaders, même quand ces derniers peuvent traiter des paquets de N pixels en une seule fois, grâce au SIMD et au FGMT. Le circuit d'arbitrage peut se manifester, si tous les processeurs de shaders sont occupés et qu'aucun ne peut prendre en charge de nouveau fragment/pixel, mais c'est assez rare. Par contre, cela pose des problèmes dans le sens inverse, à savoir quand les processeurs de shaders veulent envoyer des triangles aux rastériseurs. Dans ces conditions, il arrive que tous les processeurs de shaders veuillent envoyer des triangles au rastériseur, mais il n'y a pas assez de rastériseurs pour tous les recevoir. Dans ce cas, le circuit d'arbitrage sélectionne N triangles et met les autres en attente. Une telle situation est assez fréquente quand on exécute un ''vertex shader'' très basique, ce qui est fréquent dans les ''pré-passes z'', une optimisation des moteurs 3D "récents" que je ne détaillerais pas ici. Le ''vertex shader'' est très simple, donc le GPU peut calculer beaucoup de triangles par secondes, ce qui sature les rastériseurs. Une solution alternative remplace le réseau d'interconnexion par un bus, qui relie les rastériseurs et les processeurs de shaders. Le cablage est alors plus simple, mais les performances du bus sont inférieures. La solution a les mêmes problèmes d'arbitrage, si ce n'est pire, car plusieurs processeurs de shaders tentent d'accéder au bus en même temps. Il faut que le bus fonctionne à très haute fréquence pour que cela ne pose pas de problèmes. Quelques systèmes anciens de l'entreprise SGI utilisaient un tel 'triangle bus'', mais ils étaient bien les seuls. Le réseau d'interconnexion est plus simple avec des processeurs séparés pour les ''vertex'' et les ''pixel shaders''. Dans ce cas, la solution la plus simple est d'utiliser autant de rastériseurs qu'il y a de processeurs de ''vertex shader''. Chaque processeur de ''vertex shader'' alimentera un rastériseur, qui lui-même alimentera plusieurs processeurs pour les pixels shaders. Il suffit de relier les rastériseurs aux processeurs de ''pixel shader''. Mais au <noinclude> {{NavChapitre | book=Les cartes graphiques | prev=Le pipeline géométrique d'un GPU | prevText=Le pipeline géométrique d'un GPU | next=Les unités de texture | nextText=Les unités de texture }}{{autocat}} </noinclude> mao2g8o3n0xpzj1sgbealfjzt3mq4bk Les cartes graphiques/Sommaire 0 70681 763459 763319 2026-04-11T15:59:56Z Mewtow 31375 /* Le pipeline fixe, non-programmable */ 763459 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 : évolution|Le pipeline géométrique : évolution]] * [[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}} scz674ow3k7ymmmpnywn46k5gk4etl1 763464 763459 2026-04-11T16:01:29Z Mewtow 31375 /* Le pipeline fixe, non-programmable */ 763464 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 : évolution|Le pipeline géométrique : évolution]] * [[Les cartes graphiques/Le pipeline géométrique d'un GPU|Le pipeline géométrique d'un GPU]] * [[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}} o87epqdmqiw01q5s5wm6fr3gelcw6b7 763496 763464 2026-04-11T18:32:05Z Mewtow 31375 763496 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 : évolution|Le pipeline géométrique : évolution]] * [[Les cartes graphiques/Le pipeline géométrique d'un GPU|Le pipeline géométrique d'un GPU]] * [[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]] {{autocat}} oe2xto2ueeb6dsntlj33lp79udtsy6f Neurosciences/Les médicaments du système nerveux 0 72475 763513 761445 2026-04-11T23:33:04Z Litlok 2308 /* Les antagonistes/agonistes des neuropeptides */ remplacement de quelque soit, construction incorrecte 763513 wikitext text/x-wiki Les médicaments utilisés en psychiatrie et en neurologie agissent tous sur le système nerveux central. Et les médicaments qui agissent sur le système nerveux central ne sont pas nombreux. La raison est que le cerveau est protégé par une barrière hématoencéphalique, qui empêche de nombreuses molécules de rentrer dans le cerveau. De nombreux médicaments qui pourraient agir sur le cerveau ne peuvent donc pas y rentrer. Et c'est un gros problème pour les entreprises pharmaceutiques qui voudraient créer des médicaments utilisables en neurologie. Nous verrons cette barrière hématoencéphalique dans les chapitres sur l'anatomie dans le détail. Pour le moment, disons que les vaisseaux sanguins cérébraux sont particuliers. Leurs parois sont imperméables, sans trous pour laisser passer le plasma vers les tissus. De plus, leur surface n'a presque pas de transporteurs capables de faire traverser les molécules, ce qui fait que les molécules dissoutes dans le plasma restent dans le sang. Enfin, une couche physique produite par les astrocytes sert de seconde protection. Ceci étant dit, passons aux médicaments utilisés en psychiatrie/neurologie. Il est d'usage de parler d'anxiolytiques, d'antidépresseurs, de somnifères, de neuroleptiques ou de stimulants. Mais ces termes sont particulièrement trompeurs ! En réalité, il n'est pas rare qu'un antidépresseur soit efficace contre l'anxiété, qu'un neuroleptique soit utilisé comme somnifère, qu'un anti-psychotique serve d'antidépresseur, etc. Les psychotropes se classent assez mal selon leurs indications thérapeutiques. Les termes antidépresseurs/anxiolytiques/anti-psychotiques/somnifères et autres désignent en réalité un mécanisme d'action particulier, qui peut servir à soigner plusieurs maladies différentes. Dans ce qui suit, nous allons utiliser un autre classement des médicaments. Nous allons les classer selon les récepteurs synaptiques sur lesquels ils agissent. Nous parlerons ainsi de médicaments sérotoninergiques pour ceux qui agissent sur les récepteurs à la sérotonine, d'adrénergiques pour les récepteurs à la noradrénaline, de cholinergiques pour les récepteurs à l’acétylcholine, etc. Ce classement n'est pas parfait car il est fréquent qu'un médicament agisse sur plusieurs récepteurs à la fois. De nombreux récepteurs synaptiques se ressemblent. L'exemple typique étant les récepteurs à l'histamine et à l'acétylcholine, qui ont beaucoup de ressemblances chimiques. Aussi, les médicaments histaminiques ont souvent des effets sur le récepteur à l’acétylcholine, et réciproquement. C'est aussi le cas pour les récepteurs des monoamines : la plupart des médicaments qui agissent sur les récepteurs de la dopamine agissent aussi sur certaines récepteurs de la sérotonine. Il en est de même pour les transporteurs de recapture : par exemple le transporteur de recapture de la dopamine et de la noradrénaline sont souvent activés conjointement par les mêmes molécules. ==Les mécanismes d'action des médicaments utilisés en psychiatrie/neurologie== Les mécanismes d'action de ces médicaments sont assez variés. Les plus simples agissent sur le métabolisme des neurotransmetteurs, à savoir leur synthèse ou leur dégradation. Les autres médicaments agissent sur les récepteurs des différents neurotransmetteurs. {|class="wikitable" |+ Classification des médicaments selon leur mécanisme d'action |- ! colspan="2" | Cible thérapeutique ! Noms de la catégorie de médicament ! Mode d'action |- ! rowspan="2" | Métabolisme des neurotransmetteurs | Synthèse des neurotransmetteurs | Molécules précurseures | Augmentent la synthèse d'un neurotransmetteur |- | Dégradation des neurotransmetteurs | Inhibiteurs de la dégradation | Inhibent la dégradation d'un neurotransmetteur |- ! rowspan="4" | Action dans la fente synaptique | Libération des neurotransmetteurs | Inhibiteurs des récepteurs vésiculaires | Empêchent de charger les neurotransmetteurs dans les vésicules synaptiques |- | rowspan="2" | Récepteurs synaptiques | Agoniste/antagoniste des récepteurs synaptiques | Simulent la présence ou l'absence d'un neurotransmetteur bien précis |- | Modulateurs allostériques | Modifient la sensibilité des récepteurs synaptiques |- | Recapture des neurotransmetteurs | Inhibiteurs des récepteurs de la recapture | Saturent les synapses avec un neurotransmetteur bien précis |- |} ===Les différents types de médicaments du système nerveux central et périphérique=== Les '''agonistes ou antagonistes des récepteurs synaptiques''' agissent sur les récepteurs synaptiques. Dans le chapitre précédent, on a vu que les récepteurs sont relativement spécialisés, sensibles à un neurotransmetteur bien précis. Par exemple, certains récepteurs ne peuvent interagir qu'avec de la sérotonine, d'autres seulement avec la dopamine, et ainsi de suite. Cependant, cette sélectivité n'est pas parfaite et d'autres molécules peuvent se lier sur un récepteur. Et de nombreux médicaments psychiatrique/neurologiques agissent sur des récepteurs de neurotransmetteurs, qu'il s'agisse de récepteur à la sérotonine, au glutamate, à la dopamine, ou autres. La plupart des médicaments utilisés pour soigner des maladies psychiatriques en sont des exemples notables, qu'il s'agisse d'anti-psychotiques, d'anxiolytiques, de somnifères, ou autre. D'autres agonistes/antagonistes ont des indications purement neurologiques, comme divers médicaments anti-épileptiques, ou ceux utilisés pour soigner Alzheimer ou Parkinson. Aussi, ne vous étonnez pas si ce chapitre parle de certains médicaments utilisés en neurologies ou psychiatrie, ou de leur mécanisme d'action. Difficile de parler de récepteurs synaptiques sans dévier sur le domaine de la neuropharmacologie ! pire : beaucoup de ces médicaments ont des actions cardiaques, musculaires et autres. Ils n'agissent pas que sur le cerveau, mais aussi sur le reste du corps, qui est aussi sensible à la dopamine,; sérotonine et autres hormones. Beaucoup de ces médicaments agonistes/antagonistes de récepteurs synaptiques agissent sur des récepteurs métabotropes, couplés aux protéines G. Et pour rappel, les neurones adaptent la quantité de ces récepteurs en fonction de la demande. Ils peuvent ''down''- ou ''up''-réguler la quantité de récepteurs synaptique quand les récepteurs sont trop ou pas assez activés. Si on les soumets à des doses trop fortes d'agonistes, ils peuvent détruire des récepteurs, ce qui réduit la sensibilité du neurone au neurotransmetteur. Et inversement, des doses d'antagonistes ou d'agonistes inverses peuvent induire une augmentation de la quantité de récepteurs synaptiques. C'est en partie pour cette raison que certains médicaments deviennent de moins en moins efficaces avec le temps. L'augmentation ou diminution de la quantité de récepteur compense l'effet agoniste/antagoniste du médicament. On dit qu'ils entrainent l'apparition d'une tolérance, qui se développe en quelques jours, le temps que les récepteurs disparaissent de la surface des neurones. Cela aura une grande importance pour la suite. Une classe de médicaments similaires aux précédent est celle des '''modulateurs allostériques'''. Pour comprendre comment ils agissent, il faut savoir que de nombreux récepteurs ont une sensibilité ajustable. Le cas classique est celui des récepteurs au GABA et au glutamate, qui peuvent être plus ou moins sensibles au glutamate/GABA. La sensibilité est modulée par des "neurotransmetteurs". Je mets des guillemets car de telles molécules ne respectent pas à la lettre la définition de neurotransmetteur, ce qui fait qu'on préfère le terme de '''modulateurs allostériques'''. Les modulateurs allostériques s'attachent au récepteur, mais ne transmettent pas de potentiel d'action et n'ouvrent pas de canal ionique. Par contre, ils modifient la conformation spatiale du récepteur, qui se déforme et se réorganise, ce qui modifie sa sensibilité et son efficacité. Certains modulateurs allostériques rendent le récepteur plus sensible et ont un effet excitateur. Ils sont appelés des ''modulateurs allostériques positifs'', ou encore des ''agonistes allostériques''. D'autres rendent le récepteur moins sensible et ont un effet globalement inhibiteur. Ils sont appelés des ''modulateurs allostériques négatifs'', ou encore des ''antagonistes allostériques''. [[File:Les récepteurs membranaires comme cibles thérapeutiques.png|centre|vignette|upright=2.0|Les récepteurs membranaires comme cibles thérapeutiques]] D'autres médicaments agissent non pas sur les récepteurs synaptiques proprement dit, mais sur les transporteurs de recapture des neurotransmetteurs. Ces transporteurs de recapture permettent au neurone pré-synaptique de recycler les neurotransmetteurs émis dans la fente synaptique, après leur utilisation. Les '''inhibiteurs de la recapture''' empêchent cette recapture, ce qui fait que les neurotransmetteurs émis restent plus longtemps dans la fente synaptique et agissent plus longtemps. Le cas le plus emblématique est celui des inhibiteurs de la recapture de la sérotonine, qui sont utilisés comme anti-dépresseur, anxiolytique, traitement des troubles obsessionnels-compulsifs, anti-douleur, etc. Il existe aussi des médicaments qui facilitent l'émission d'un neurotransmetteur dans la fente synaptique Par exemple, il existe des médicaments qui facilitent l'émission de dopamine ou de noradrénaline par les neurones pré-synaptique. Ces '''facilitateurs de l'émission d'un neurotransmetteur''' sont cependant rares et peu utilisés. D'autres au contraire, réduisent l'émission des neurotransmetteurs dans la fente synaptique. La plupart bloquent le chargement des neurotransmetteurs dans les vésicules synaptiques. Ce chargement est le fait de transporteurs, situé sur les vésicules, qui sont souvent spécialisées pour un neurotransmetteur précis. Les médicaments qui bloquent ces transporteurs sont appelés les '''inhibiteurs des transporteurs vésiculaires'''. D'autres médicaments agissent sur le métabolisme des neurotransmetteur, sans action sur les récepteurs synaptiques ou l'émission des neurotransmetteurs. Les plus simples sont des '''molécules précurseurs''', qui sont utilisées dans la synthèse des neurotransmetteurs, comme la 5-HT ou la dévodopa (L-DOPA). Ils sont utilisés dans les maladies où survient une déficience en un neurotransmetteur particulier. Par exemple, la maladie de Parkinson entraine une déficience en dopamine intra-cérébrale, qui est corrigée en donnant au malade de la L-DOPA, qui est convertie en dopamine par les mécanismes vus dans les chapitres précédents. Mais il s'agit là d'un cas assez rares, l'utilisation de molécules précurseurs étant peu utilisée, les déficiences en neurotransmetteurs étant rares. Il faut enfin citer les '''inhibiteurs des enzymes de dégradation'''. Ils inhibent la dégradation de certains neurotransmetteurs, en agissant directement sur l'enzyme responsable de la dégradation. Les neurotransmetteurs censés être dégradés s'accumulent alors dans les synapses, y restent plus longtemps. Par exemple, certaines molécules inhibent la monoanime-oxydase, qui dégrade sérotonine, dopamine et noradrénaline. Ils sont utilisés pour saturer les synapses de ces trois neurotransmetteurs. ===L'effet des médicaments du système nerveux central et périphérique=== L'effet des médicaments du système nerveux central et périphérique est soit de créer/simuler un excès de neurotransmetteur, soit de créer/simuler une déficience. Par exemple, les inhibiteurs de la recapture et inhibiteurs de la dégradation saturent les synapses en neurotransmetteurs, au même titre que les facilitateurs de l'émission. Ils ont tendance à créer un excès de neurotransmetteurs dans les synapses. Les agonistes des récepteurs synaptiques simulent un excès dans le neurotransmetteur considéré. Par simulent, on veut dire que le neurotransmetteur n'est pas en excès, mais que c'est tout comme du point de vue des récepteurs synaptiques. À l'inverse, les antagonistes et agonistes inverse simulent une pénurie de neurotransmetteur : les neurotransmetteurs sont toujours produits et émis dans la fente synaptique, mais ils n'agissent pas sur les récepteurs synaptique ou leur action est compensée. Du point de vue du neurone, tout se passe comme s'il y avait déficience en neurotransmetteur. L'effet dépend alors du neurotransmetteur considéré. En soi, les réelles déficiences ou excès de neurotransmetteurs sont rares. Il y a bien le cas de la maladie de Parkinson, où on observe une déficience en dopamine, mais c'est plus une exception qui confirme la règle. Les autres maladies du système nerveux n'impliquent pas de déficience ou d'excès réel. Ce qui ne veut pas dire que les médicaments ne servent à rien, ni qu'il n'ont qu'une utilité purement symptomatique. Les indications de chaque classe de médicament est donnée dans le tableau ci-dessous. {|class="wikitable" |+ Classification des médicaments agissant sur le système nerveux |- ! Mécanisme d'action ! Indications |- ! colspan="2" | Action sur les canaux ioniques |- | Bloqueurs des canaux ioniques au sodium | * Épilepsie * Troubles bipolaires, notamment pour les états maniaques * Traitement des douleurs neuropathiques (d'origine neurologique) * Troubles du sommeil et troubles anxieux (rare) * Anesthésie locale |- ! colspan="2" | GABAergiques |- | Modulateurs allostériques positifs du récepteur GABA-A : barbituriques, benzodiazépines, non-benzodiazépines | * Troubles du sommeil * Troubles anxieux * Épilepsie * Catatonie * Anesthésie générale (pour la perte de conscience) * Décontractants musculaires * Agitation sévère. |- ! colspan="2" | Histaminergiques |- | Antagonistes ou agoniste inverse des récepteurs à l'histamine H1 (Anti-histaminiques anciens) | * Troubles du sommeil * Troubles anxieux * Mal des transports |- ! colspan="2" | Sérotoninergiques |- | Inhibiteurs sélectifs de la recapture de la sérotonine | rowspan="3" | * Dépression * Troubles anxieux * Troubles obsessionnels-compulsifs * Troubles du comportement alimentaire * Troubles impulsifs * Autres troubles psychiatriques/neurologiques. * Rarement utilisés comme antidouleur dans les cas de douleurs d'origine neurologique ou supposée neurologique (fibromyalgie, autres) |- | Inhibiteurs de la recapture de la sérotonine et de la noradrénaline (bloquent les deux à la fois) |- | Inhibiteurs de la monoamine-oxydase |- ! colspan="2" | Dopaminergiques |- | Antagoniste de la dopamine. | * Psychoses (schizophrénie, délires) * Épisodes maniaques des bipolaires * Agitation sévère * Vomissements et nausées |- | Agoniste de la dopamine. | rowspan="2" | * Drogues d'abus * Maladie de Parkinson * Narcolepsie et hypersomnies * Trouble déficitaire de l'attention |- | Inhibiteurs de la recapture de la dopamine/noradrénaline |- ! colspan="2" | Noradrénergiques |- | Inhibiteurs de la recapture de la noradrénaline | rowspan="2" | * Anxiolytiques et antidépresseurs * Narcolepsie * Trouble déficitaire de l'attention (ADHD) * Médicaments coupe-faim |- | Facilitateurs de l'émission synaptique de la noradrénaline |- | Bêtabloquants (antagonistes des récepteurs béta-adrénergiques) | * Cardiologie : troubles du rythme cardiaque, insuffisance cardiaque, hypertension, autres. * Anxiété de performance, à savoir le trac avant une présentation orale, le stress avant un examen, etc. |- ! colspan="3" | Cholinergiques |- | Inhibiteurs de l'acétylcholinestérase | * Myasthénie ''gravis'' et autres maladies de la jonction neuromusculaire |- | Antagonistes des récepteurs cholinergiques | * Anesthésie locale ou générale, pour paralyser les muscles |- ! colspan="3" | Opioïdes / morphiniques |- | Agonistes des récepteurs opioïdes | * Anti-douleurs (analgésiques) * Drogues d'abus * Anti-diarrhéiques (sans lien avec leur action sur le système nerveux central) |- | Antagonistes des récepteurs opioïdes | * Traitement des overdoses d'opioïdes * Laxatifs (action purement périphérique, sans lien avec le système nerveux central) |- ! colspan="3" | Neuropeptidergiques |- | Antagonistes des récepteurs à la substance P | Anti-émétiques (anti-vomissements, anti-nausées). |} Comme vous pouvez le constater en lisant ce tableau ci-dessus, un médicament neuropsychiatrique peut soigner des maladies très différentes. Par exemple, les sérotoninergiques traitent un grand nombre d'affections psychiatriques et neurologiques, allant de la dépression aux douleurs neuropathiques. Idem avec les bloqueurs des canaux ioniques et les GABAergiques, qui sont prescrits dans des indications très variées. Et inversement une même maladie peut être soignée par des médicaments neuropsychiatriques aux modes d'action totalement différents. Le cas le plus parlant est celui de l'anxiété, qui peut être traité en influençant le GABA, la sérotonine, la noradrénaline, l'histamine, le glutamate, voire les canaux ioniques. Pareil pour les troubles du sommeil, qui peuvent être influencés par les GABAergiques, les antihistaminiques, les bloqueurs des canaux ioniques, les opioïdes, les cannabinoïdes, et j'en passe. ==Les histaminergiques : sommeil, anxiété, mal des transport== Avant de voir la sérotonine, la dopamine et la noradrénaline, nous allons aborder les médicaments de l'histamine. Pour rappel, l'histamine stimule l'éveil. Il existe quatre récepteurs pour l'histamine, dont seulement deux se trouvent dans le cerveau. Les deux récepteurs cérébraux sont le récepteur H1 et le récepteur H3. Le récepteur H1 est un récepteur "normal", qui se trouve sur les neurones post-synaptiques, alors que le H3 est un auto-récepteur pré-synaptique. Et cette différence est très importante pour ce qui va suivre. Il existe des médicaments qui empêchent l'histamine d'agir sur les récepteurs H1. D'où leur nom d''''antihistaminiques H1'''. Comme exemples, on peut citer la doxylamine, la niaprizine, l’alimémazine ou encore la prométhazine. Ils font dormir et réduisent l'anxiété, mais sont rarement utilisés comme somnifères et anxiolytiques. Et quand ils le sont, c'est surtout en seconde intention. La raison est qu’ils sont peu efficaces et ont des effets secondaires assez importants. Leur demi-vie longue fait qu'ils entraînent une somnolence diurne, par exemple. De plus, ils ont une action antagoniste sur les récepteurs cholinergiques, qui sont structurellement semblables aux récepteurs à l'histamine, ce qui est la cause de beaucoup d'effets secondaires : bouche sèche, réduction de la transpiration, constipation, troubles du rythme cardiaque, etc. Ils sont utilisés pour traiter les formes mineures d'anxiété : problèmes d'endormissements mineurs, réduire la tension avant un examen, plus rarement comme prémédication avant une anesthésie, etc. Par contre, ils ne sont pas utilisés pour soigner les troubles anxieux de nature psychiatriques. L'effet le plus souvent recherché est anti-naupathique, à savoir contre le mal des transports. L'explication à cela sera abordée dans le chapitre sur l'équilibrioception. Pour ce qui est des récepteurs H3, il existe des antagonistes et des agonistes inverses. Le récepteur H3 est un auto-récepteur, c'est à dire qu'il "mesure" la quantité d'histamine dans la fente synaptique, et inhibe le neurone pré-synaptique si celle-ci est trop élevée. En clair, ce récepteur empêche de trop émettre d'histamine dans les synapses. Les antagonistes/agonistes inverse de ce récepteur bloquent ce processus. Le résultat est que les neurones pré-synaptiques émettent plus d'histamine dans les synapses. Le patient est alors plus éveillé. Les indications thérapeuthique sont la narcolepsie. ==Les sérotoninergiques : les mal-nommés antidépresseurs== Les médicaments sérotoninergiques sont souvent appelés des antidépresseurs dans le sens commun, ce qui est un abus de langage. La confusion entre sérotoninergiques et antidépresseurs vient du fait que tous les antidépresseurs récents sont sérotoninergiques, et réciproquement. Mais ça n'a pas toujours été le cas. Le premier antidépresseur découvert était la réboxétine, un inhibiteur de la recapture de la noradrénaline, sans action sur la sérotonine. Il a été suivi par les inhibiteurs de la mono-amine oxydase, qui agissaient sur la dopamine, la noradrénaline et la sérotonine. Les médicaments plus récents sont plus sélectifs et ne ciblent que la sérotonine, afin de limiter les effets secondaires. Au passage, si les antidépresseurs actuels sont des sérotoninergiques, réduire les sérotoninergiques à des antidépresseurs est vraiment trompeur, même si leur indication principale est la dépression. Ils sont aussi le traitement de référence des troubles anxieux, des troubles obsessionnels-compulsifs, des troubles des impulsions et de quelques autres affections neuropsychiatriques. Les sérotoninergiques sont le traitement de référence de tous les troubles anxieux, à l'exception des phobies simples : anxiété sociale, anxiété généralisée, troubles de stress post-traumatique, etc. En clair, les antidépresseurs sont aussi des anxiolytiques, même si les anxiolytiques sont souvent confondus avec les GABAergiques, d'où une certaine confusion. Les raisons à cela sont multiples et viennent surtout de l'histoire médicale : il a fallu du temps avant qu'on se rende compte de l'effet anxiolytique des sérotoninergiques, alors qu'on connaissait déjà des anxiolytiques GABAergiques et que les dénominations étaient déjà bien implantées avant cette découverte. Toujours est-il que l'effet anxiolytique des antidépresseurs est réel et souvent très efficace, plus que pour les autres traitements. Ils sont le traitement de première intention à l'heure actuelle en raison de leur profil d'effets secondaires plus intéressant que pour les autres anxiolytiques (pas de dépendance, surtout). Outre l'anxiété ''stricto sensus'', les antidépresseurs sont utilisés dans le traitement des troubles obsessionnels-compulsifs, à des doses supérieures à celles de la dépression. Un effet notable sur le sommeil des sérotoninergiques est une réduction du sommeil paradoxal assez marquée. Ils peuvent aussi causer des rêves bizarres ou des cauchemars assez importants. Leur effet sur le sommeil paradoxal est utilisé par les médecins, pour réguler le sommeil dans certaines pathologies où le sommeil paradoxal est déréglé. Il arrive aussi que les sérotoninergiques soient utilisés pour améliorer le sommeil. Par contre, ils ne sont pas des hypnotiques dans le sens où ils ne font pas dormir. Certaines molécules ont la réputation d'être sédatives et d'autres stimulantes, cela reste une réputation en-dehors de quelques médicaments agissant aussi sur l'histamine (mirtizapine). Ils sont aussi utilisés pour le traitement des douleurs chroniques, particulièrement des douleurs dites neuropathiques, qui ont une origine neurologique. Les molécules utilisées dans cette indications agissent à la fois sur la sérotonine et la noradrénaline. Les autres indications sont encore assez expérimentales, que ce soit pour l'impulsivité, ou autre. ===Les sous-types d'antidépresseurs=== Les anti-dépresseurs agissent tous sur au moins l'un des trois neurotransmetteurs suivants : la dopamine, la noradrénaline et la sérotonine. Les antidépresseurs les plus couramment utilisés, à l'heure actuelle, agissent uniquement sur la sérotonine, car ils ont moins d'effets secondaires que les autres, et sont plus efficaces que les noradrénergiques ou les dopaminergiques purs. On peut classer les anti-dépresseurs en plusieurs classes, dont les plus connues sont les inhibiteurs de la monoamine-oxydase (IMAO), les tricycliques et les inhibiteurs de la recapture de la sérotonine. On ne sait pas si ces différentes classes ont des différences d'efficacité, mais on a de bonnes raisons de penser que toutes les classes se valent, avec peut-être un effet des IMAO et tricycliques sur les dépressions résistantes aux autres médicaments. <noinclude>[[File:Amezepine.svg|vignette|upright=0.5|Molcule d'Amezepine, un tricyclique.]] [[File:Mirtazapine.svg|vignette|upright=0.5|Molécule de Mirtazapine, un tétracyclique.]]</noinclude> Les '''inhibiteurs sélectifs de la recapture de la sérotonine''' inhibent la recapture de la sérotonine. L'adjectif sélectif dans leur nom indique qu'ils n'ont pas ou peu d'effets sur la noradrénaline ou la dopamine, ou tout autre neurotransmetteur, ce qui limite leurs effets secondaires. Les seuls effets secondaires sont limités à des effets secondaires sexuels (impuissance, éjaculation retardée, baisse de libido), neuropsychiatriques et digestifs, si on omet les allergies et intolérances. La molécule la plus connue dans cette classe est la Fluoxétine, vendue autrefois sous le nom de Prozac. Les inhibiteurs de recapture de la noradrénaline étaient utilisés autrefois comme antidépresseurs et anxiolytiques. Le tout premier antidépresseur, la réboxétine, était d'ailleurs un IRN. Il a été découvert par hasard, suite à des essais cliniques comme traitement dans d'autres indications et a été le premier antidépresseur commercialisé. Mais il s'est avéré par la suite que leur efficacité contre la dépression est au mieux faible, voire totalement inefficace. Les '''inhibiteurs sélectifs de la recapture de la noradrénaline et de la sérotonine''', ce qui veut dire qu'ils empêchent la recapture de ces neurotransmetteurs pour recyclage par les neurones. Ils n'agissent pas ou peu sur la recapture de la dopamine et sont donc sélectifs sur la sérotonine et la noradrénaline. Du fait de leur action sur la recapture de la noradrénaline, ils ont plus d'effets secondaires, sans pour autant que leur efficacité sur la dépression soit vraiment probant. Suivant la molécule de utilisée, l'effet sur la recapture sera plus orienté sur la sérotonine ou sur la noradrénaline. Par exemple, la duloxetine, la venlafaxine, et la desvenlafaxine ont un effet sérotoninergique plus prononcé, alors que le milnacipran et le levomilnacipran ont un effet noradrénergique plus probant. Les '''tricycliques''' et '''tétracycliques''' sont aussi des inhibiteurs de la recapture de la sérotonine et de la noradrénaline, mais ils ne sont pas sélectifs, c'est à dire qu'ils ont aussi une action sur d'autres récepteurs, notamment sur les récepteurs à l'acétylcholine ou encore les récepteurs à l'histamine. Ce qui fait qu'ils ont encore plus d'effets secondaires que les médicaments précédents. Leur nom vient de leur structure moléculaire, qui contient trois à quatre cycles benzéniques. Là encore, suivant la molécule de utilisée, l'effet sur la recapture sera plus orienté sur la sérotonine ou sur la noradrénaline. Certains tricycliques ont un effet plus fort sur la recapture de la sérotonine, d'autres plus sur la noradrénaline. Les '''inhibiteurs de la monoamine-oxydase''' inhibent le fonctionnement des diverses monoamine-oxydases (pour rappel : des enzymes qui dégradent les monoamines). Leur effet est de saturer les synapses en monoamines, surtout en sérotonine, noradrénaline et sérotonine. Ils peuvent cibler soit la monoamine-oxydase A, soit la monoamine-oxydase B. Ils sont aujourd'hui utilisés en dernière intention, en raison de leurs effets secondaires assez importants. Par exemple, ils interagissent assez mal avec la tyramine, un acide aminé présent dans beaucoup d'aliments et un dérivé du métabolisme des catécholamines. Pour rappel, la tyramine est une ''trace amine'', donc une monoamine, avec un effet sur le système nerveux et le corps en général. La tyramine est dégradée par les monoamine-oxydases, en temps normal. Mais avec un inhibiteur des monoamine-oxydases, la tyramine ne sera plus autant dégradée et la quantité de tyramine augmente dans le sang, causant une crise hypertensive potentiellement mortelle. Les patients doivent éviter de consommer du fromage, de l'alcool, du foie et plein d'autres aliments riches en tyramine, pour éviter tout problème. La tyramine produite par le corps n'est pas un problème, car elle est présente à l'état de traces, en très très faibles quantités. Outre ces trois classes principales, on trouve d'autres médicaments plus rarement utilisés. On peut par exemple citer le bupropion qui est un inhibiteur de la recapture de la dopamine/noradrénaline et quelques autres molécules moins communes qui agissent comme agoniste inverse/antagoniste des récepteurs à la sérotonine. Les dopaminergiques comme le modafinil peuvent encore être prescrits dans certains pays pour soigner la dépression, mais cet usage est très rare et rarement justifié. ===Les effets secondaires=== Les effets secondaires les plus courants sont des troubles bénins et communs : nausées et vomissements, insomnies, fatigue, somnolence, etc. Viennent ensuite les troubles d'ordre sexuels : difficultés d'érection, impuissance, priapisme, etc. L'arrêt des antidépresseurs peut, bien que ce soit assez peu fréquent, entraîner un '''syndrome de discontinuation''' (autrefois appelé à tort "syndrome de sevrage"). Il se manifeste par des symptômes assez variés : nausées, vomissements, diarrhées, petite fièvre, syndrome grippal, insomnie, somnolence, cauchemars, tremblements, vertiges, etc. L'existence de ce syndrome ne signifie PAS que les antidépresseurs sont addictifs ou qu'ils entraînent une tolérance. En réalité, ce syndrome vient du fait que le corps doit s'adapter à une baisse du taux de sérotonine lors de l'arrêt du traitement. Le nombre de récepteurs sérotoninergiques s'adapte assez brutalement, ce qui entraîne quelques symptômes gênants mais rarement graves. On peut éviter l'apparition de ce syndrome en réduisant progressivement les doses, par exemple en les divisant par deux, ou en les diminuant de quelques milligrammes, toutes les deux/trois semaines. Les antidépresseurs sont aussi connus pour entraîner des épisodes maniaques, surtout chez les patients bipolaires. Les antidépresseurs sont, chez les bipolaires, systématiquement prescrits avec des régulateurs de l'humeur, des médicaments qui calment les états maniaques sans rendre dépressif. Il arrive très rarement (quelques pourcents des patients) que des patients non diagnostiqués bipolaires déclenchent un épisode maniaque sous antidépresseur, souvent au début du traitement. Dans ce cas, cela ne signifie pas forcément que le patient est bipolaire, le diagnostic demandant un épisode maniaque en-dehors de tout traitement médicamenteux, même si c'est quand même mauvais signe. Les patients non-bipolaires qui déclenchent un épisode maniaque sous antidépresseur ont plus de 30% de chances d'évoluer vers un vrai trouble bipolaire. Les psychiatres se battent encore pour savoir si de tels épisodes chez un patient sain signifie ou non la présence d'un trouble bipolaire. Des chercheurs psychiatres supposent que de tels épisodes sont le signe d'une forme atténuée de trouble bipolaire, parfois nommée "trouble bipolaire de type 3", qui se manifeste par des dépressions accompagnées d'épisodes hypomaniaques sous antidépresseurs. D'autres chercheurs pensent que des épisodes induits par des antidépresseurs ne sont que des effets secondaires du traitement, du moins chez la plupart des patients. Plus rarement, les patients peuvent développer un '''syndrome sérotoninergique''', causé par une overdose de sérotonine, aux symptômes divers. Ce syndrome apparaît presque exclusivement dans le cas d'une interaction médicamenteuse, quand on combine un antidépresseur avec d'autres médicaments sérotoninergiques, les exceptions étant des overdoses franches d'antidépresseurs souvent reliées à des tentatives de suicide. Il se manifeste par des symptômes autant psychiatriques que neurologiques et végétatifs. Les cas peu dangereux se manifestent par de la nausée, des diarrhées, des insomnies, de la nervosité, un comportement légèrement agressif et quelques autres symptômes peu graves. Dans les cas sévères, il peut se manifester par un coma, une confusion ou des délires hallucinatoires, ou de l'épilepsie. Les cas les plus graves se couplent de fièvre, ainsi que de troubles respiratoires et cardiaques. De manière générale, le patient manifeste plusieurs symptômes parmi les suivants, suite à une surdose de sérotoninergiques : * des symptômes psychiatriques : agitation, hypomanie, hallucinations et/ou délires, confusion, insomnie ; * des symptômes neurologiques : tremblements, rigidité musculaire, crise épileptique, réflexes plus intenses que d'habitude (hyper-réflexie), défaut de coordination des mouvements (ataxie), akathisie, dilatation des pupilles (mydriase) ; * des symptômes végétatifs : tachycardie, accélération de la respiration (tachypnée), baisse ou élévation de la pression artérielle, sueurs, fièvre, nausée , diarrhée. ==Les dopaminergiques : stimulants, antipsychotiques, Parkinson== Les médicaments dopaminergiques sont assez nombreux, la plupart étant des médicament antagonistes de la dopamine, plus rarement agonistes. On peut aussi citer les antagonistes de la recapture de la dopamine, dont la plupart sont utilisés comme drogues. Mais au-delà de cette utilisation, les dopaminergiques sont aussi utilisés comme anti-parkinsoniens, ou comme anti-psychotiques. ===Les stimulants dopaminergiques=== Les '''stimulants''' sont des médicaments qui augmentent la vigilance et favorisent l'éveil. Les stimulants ont des modes d'action variés, mais les plus communs ont des dopaminergiques avec une action noradrénergique secondaire. D'ailleurs, l’appellation ''stimulant'' est souvent utilisée comme synonyme de "dopaminergique". Les dopaminergiques ont tous les mêmes effets : élévation de l'humeur pouvant aller jusqu’à l'euphorie, augmentation de l'activité (hyperactivité), accélération motrice et psychique, amélioration de la vigilance, réduction de la fatigue et des besoins de sommeil, amélioration de la confiance en soi, désinhibition et impulsivité, parfois agressivité ou irritabilité, coupent l'appétit, etc. Les effets à forte dose sont assez similaires aux symptômes des états maniaques observés chez les bipolaires, ce qui traduirait une altération dopaminergique chez ces patients. À très fortes doses, ils entraînent des symptômes psychotiques : délires, hallucinations, désorganisation de la pensée et du comportement. Du fait que leur action sur la vigilance et l'éveil, ils sont utilisés pour le traitement de la narcolepsie : ils améliorent l'état d'éveil de ces patients, ce qui les empêche de s’endormir spontanément. En psychiatrie, ils sont utilisés pour soigner le trouble déficitaire de l'attention, ou TDAH, un trouble caractérisé par une hyperactivité, une inattention et de l'impulsivité. Les agonistes dopaminergiques ont tendance à améliorer l'état de ces patients, alors qu'ils ont l'effet inverse chez les sujets sains ! Rarement, ils sont utilisés comme anti-dépresseur, voire comme coupe-faim. C'était notamment le cas du malheureusement connu médiator, qui était utilisé pour son effet anorexigène. Les agonistes dopaminergiques utilisés comme médicaments mais aussi à des fins bien moins recommandables. Tous, sans exception, ont un potentiel addictif majeur, ce qui explique que certains soient utilisés comme drogues. Formellement, la cocaïne et l’héroïne ont un effet dopaminergique avéré qui explique leur caractère addictif destructeur. De ce fait, les stimulants sont sévèrement contrôlés et ne sont prescrits que dans des cas bien spécifiques. : Leur effet dans le TDAH fait penser à certains que ces médicaments améliorent l'attention et auraient un effet pro-cognitif, mais les résultats expérimentaux sont contrastés... Ce qui n’empêche pas leur usage détourné pour réviser aux examens ou comme drogue de performance. Les stimulants plus utilisées à l'heure actuelle sont le '''modafinil''' et la '''Ritaline''' (méthylphénidate). Leur mode d'action est assez précis : ce sont des inhibiteurs de la recapture de la dopamine. Leurs autres modes d'action sont mineurs, contrairement aux autres phényléthylamines. Ils sont utilisés dans le traitement du trouble déficitaire de l'attention et de l'hypersomnie, notamment la narcolepsie. <noinclude>[[File:Methylphenidate stereoisomers labelled.svg|centre|vignette|upright=1.5|Methylphenidate, stereoisomers labelled]] [[File:Modafinil enantiomers labelled.svg|centre|vignette|upright=1.5|Modafinil, enantiomers labelled]]</noinclude> Nous mettons de côté les amphétamines, car leur mode d'action est différent de celui du ''modafinil'' et de la ''Ritaline''. Elles réduisent la recapture de la dopamine, mais aussi de la noradrénaline. Mais elles n'agissent pas directement sur les récepteurs de recapture de la dopamine/noradrénaline. A la place, elles agissent sur le récepteur TAAR1, ainsi que sur les vésicules synaptiques. En plus de bloquer la recapture de la dopamine et de la noradrénaline, elles augmentent l'émission synaptique de dopamine. ===Les anti-parkinsoniens=== La maladie de Parkinson est, sans rentrer dans les détails que nous verrons dans un chapitre dédié au vieillissement du cerveau, une maladie neurologique causée par la mort de neurones dopaminergiques dans une zone bien précise du cerveau appelée la substance noire. Elle est traitée par des traitements symptomatiques, qui compensent le déficit de dopamine dans le cerveau. Si le traitement symptomatique marche relativement bien dans la plupart des cas, tous ces traitements peuvent avoir des effets secondaires assez marqués, causés par un excès de dopamine cérébrale. Les plus communs sont les syndromes psychotiques, caractérisés par des hallucinations et/ou idées délirantes sont notamment communes. Les hallucinations liées à ces traitements sont quasiment toujours des hallucinations visuelles, les idées délirantes sont souvent des idées paranoïaques. Les effets secondaires cardiaques sont aussi à prendre en compte, notamment au niveau de la tension artérielle. Le médicament de référence est la '''lévodopa''', un précurseur de la dopamine qui est transformé en dopamine dans le cerveau. Cependant, il est rarement utilisé seul du fait de sa "faible" efficacité en mono-thérapie. Il faut dire qu'une bonne partie de la lévodopa est dégradée par la dopa-décarboxylase avant d'atteindre le cerveau. Cela tend à maximiser les effets secondaires (cardiaque, notamment)s, tout en réduisant l'efficacité du traitement sur le cerveau. Pour éviter cela, ce médicament est consommé avec un inhibiteur de la dopa-décarboxylase, ce qui limite la dégradation périphérique, sans agir sur la transformation en dopamine dans le cerveau. Ce traitement est donné en première intention, notamment chez les sujets âgés, qui le supportent mieux. D'autres traitements sont proposés une fois que la lévodopa commence à perdre en efficacité avec le temps. Ces traitements fonctionnent soit en augmentant la production de dopamine, soit en limitant sa recapture, soit en réduisant sa dégradation. Par exemple, on peut utiliser des médicaments qui réduisent la dégradation de la dopamine en inhibant soit la COMT, soit la monoamine-oxydase. On peut aussi utiliser des agonistes dopaminergiques, comme le modafinil ou des inhibiteurs de recapture de la dopamine. ===Les antipsychotiques=== Les antipsychotiques sont utilisés en psychiatrie pour soigner les troubles psychotiques et le trouble bipolaire. Pour rappel, une psychose se caractérise par la présence d'au moins un des trois symptômes suivants : * délire (paranoïa, mégalomanie, érotomanie, autre) ; * hallucinations (le plus souvent auditives, plus rarement visuelles ou autres) ; * désorganisation de la pensée et de la parole (le terme technique est : troubles de la pensée formelle). Une psychose est un syndrome qui peut apparaitre suite à un trouble neurologique ou la consommation de stupéfiants, mais la cause la plus commune est une schizophrénie ou un état maniaque sévère. La maladie psychotique par excellence est la schizophrénie, qui est une psychose de longue durée, mais on pourrait aussi citer des troubles similaires (troubles schizophréniformes, bouffées délirantes aiguës, autres). On observe aussi des psychoses dans certains cas très sévères de dépression (dépression psychotique), ou dans les troubles bipolaires. Les antipsychotiques sont aussi utilisés pour calmer les états maniaques des patients bipolaires, indépendamment de toute psychose. Ces états maniaques impliquent divers symptômes présents à des degrés divers : humeur euphorique et/ou irritable, "hyperactivité", accélération de la pensée et de la parole, insomnie massive par réduction des besoins de sommeil, apparition de comportements impulsifs ou dangereux, grandiosité pouvant aller jusqu'à un délire mégalomaniaque, ... Les antipsychotiques calment ces patients et les font revenir à la normale. Leur effet "calmant" fait qu'ils sont aussi indiqués les états d'agitation et d’agressivité, dans le cadre des démences, de l'autisme ou de la psychopathie. Les antipsychotiques sont aussi appelés des neuroleptiques. Ils sont les traitements de choix pour les psychoses et états maniaques. Leur découverte a été une révolution dans le domaine de la psychiatrie, permettant enfin de soigner des patients et de leur quitter les asiles où ils restaient enfermés en attendant une éventuelle rémission... Il existe plusieurs types d'antipsychotiques, aux mécanismes d'action fortement différents. Mais tous agissent sur la dopamine, soit en réduisant la libération de la dopamine, soit en réduisant leur action sur les neurones (antagoniste des récepteurs à la dopamine). Ils peuvent aussi agir sur d'autres neurotransmetteurs, généralement des monoamines, mais leur action sur la dopamine est considérée comme leur principal mode d'action. Les '''dépleteurs des monoamines''' sont des antipsychotiques, qui réduisent non seulement la dopamine, mais aussi la noradrénaline, et toutes les monoamines. Ils agissent sur les transporteurs vésiculaire des monoamine. Pour rappel, il existe un transporteur vésiculaire qui marche pour toutes les monoamine, appelé le VMAT 2. Inhiber ce transporteur empêche les vésicules synaptiques de fonctionner correctement. Les monoamines sont donc synthétisées, mais pas libérées dans les synapses en grande quantité. Les médicaments de ce type étaient utilisés utilisés comme anti-psychotiques, car ils réduisent les taux de catécholamines intra-cérébraux. De nos jours, leur usage principal est le traitement de l'hypertension. La réserpine est le médicament le plus connu de cette classe. les antispcyhotiques les plus utilisés actuellement sont les '''antagonistes de la dopamine''', qui empêchent la dopamine d'agir sur les récepteurs dopaminergiques. Ceux actuellement utilisés en pratique clinique ont une affinité toute particulière pour les récepteurs dopaminergiques de type D2. En général, leur action sur les récepteurs à la sérotonine ou l'acétylcholine est faible, et dépend de la molécule. Par exemple, certains anti-psychotiques agissent sur les récepteurs de la sérotonine (c'est le cas de la clozapine), mais d'autres ne le font pas. Leur action sur les récepteurs à la dopamine a cependant d'autres conséquences, qui sont une cause d'effets secondaires. Le plus classique est un syndrome parkinsonien (tremblements, immobilité, rigidité musculaire) induit par les neuroleptiques, qui recède à l'arrêt du traitement. Il provient de l'action des médicaments sur les ganglions de la base, et notamment sur la substance noire et les autres structures dopaminergiques de ces aires cérébrales. C'est pour cette raison que les neuroleptiques sont contrindiqués chez les parkinsoniens, à l'exception de la dompéridone qui n'entre pas dans le cerveau (elle ne traverse pas la barrière hémato-encéphalique). Il y a aussi une action sur les voies dopaminergiques qui innervent l'hypophyse/hypothalamus, deux glandes intracérébrales. L'effet est une augmentation de la production de la prolactine, une hormone impliquée dans le développement des seins et la lactation. Les patients peuvent voir leurs seins pousser, avoir des montées de lait, mais aussi une infertilité et d'autres effets secondaires plus handicapants. À noter que ces effets font que les neuroleptiques sont parfois détournés pour favoriser la lactation chez certaines femmes venant d'avoir un enfant. Cela explique aussi que ces médicaments sont contrindiqués chez les patients qui ont des tumeurs hypophysaires qui secrètent de la prolactine. Enfin les effets secondaires cardiovasculaires sont aussi assez fréquents et impliquent de la tachycardie ou de l'hypotension. Plus rarement, les patients peuvent développer un épisode catatonique malin (avec dysfonctionnement du système nerveux végétatif), appelé '''syndrome malin des neuroleptiques'''. Les antagonistes de la dopamine sont aussi utilisés pour leur effet anti-émétique. La plupart des anti-vomitifs disponibles en pharmacie sont d'ailleurs des neuroleptiques dits cachés. C'est le cas de la dompéridone (motillium), du primperan, du vogalène, et de bien d'autres encore. Leur effet est lié à la présence de récepteurs dopaminergiques dans le centre du vomissement, l'aire cérébrale chargée de ce réflexe digestif. ==Les GABAergiques : les mal-nommés anxiolytiques et somnifères== Les GABAergiques regroupent de nombreuses molécules, aux modes d'action assez semblables à ceux vus plus haut. Les GABAergiques cherchent tous à augmenter la quantité de GABA dans les synapses, ou à faire comme si. Il n'y a pas, à ma connaissance, de médicament qui visent à réduire la transmission GABAergique, tous l'augmentent. Ils ont un effet inhibiteur sur le cerveau, en réduisant son activité électrique générale, ce qui fait que ce sont de très bons anti-épileptiques. Mais leurs effets ne s’arrêtent pas là. Ils peuvent aussi servir contre l'anxiété ou les troubles du sommeil. ===Effets et indications=== Ils sont utilisés comme anti-épileptique, mais pas que. Leurs effets principaux sont parfois résumés par l'acronyme HAMAC : Hypnotique, Anxiolytique, Myorelaxant, Amnésiant, anti-Convulsivant. Ils servent à traiter l'anxiété et les troubles du sommeil, d'anti-épileptiques, de décontractants musculaires, mais aussi à traiter la catatonie et le ''delirium tremens'' (un syndrome de sevrage à l'alcool extrêmement sévère), etc. Ils sont aussi utilisés comme somnifères, encore que certaines molécules ne soient pas idéales pour ça. L'utilisation comme somnifère tient dans la demi-vie, à savoir le temps mis pour réduire de moitié la concentration sanguine du médicament. Plus la demi-vie est courte, plus le médicament est éliminé rapidement du sang et plus son effet est court. Un somnifère a généralement une demi-vie courte, pour éviter une somnolence diurne (l'effet du médicament doit se dissiper avant le réveil pour cela). Par contre, on attend d'un anxiolytique et d'un anti-épileptique une action prolongée, qui impose une demi-vie assez longue, d'approximativement 24 heures ou plus. Une action prolongée est aussi utile dans les insomnies où le patient se réveille trop tôt dans la matinée. L'amnésie est un effet secondaire de ces médicaments. Il s'agit d'une '''amnésie antérograde''', à savoir que les patients oublient ce qu'ils ont fait durant quelques heures après la prise du médicament, mais ils n'oublient pas leur passé. Comme autre effet secondaire, ils peuvent aussi entrainer une '''dépression respiratoire''', une réduction très dangereuse de la respiration. C'est la raison pour laquelle les insuffisants respiratoires ne peuvent pas prendre ce genre de médicaments, tout comme les patients atteints d'apnée du sommeil. Certains patients peuvent subir des '''réactions paradoxales''', à savoir des réactions opposées à l'effet sédatif du médicament. Lorsque cela arrive, le traitement peut aggraver l'anxiété et l'insomnie, désinhiber le patient, le stimuler, le rendre agressif ou agité, déclencher des comportements violents, etc. Dans certains cas assez rares, ces traitements peuvent déclencher des épisodes maniaques, voire psychotiques, quand ce n'est pas des déliriums. De très nombreux GABAergiques entraînent une forte '''dépendance''', qui s'installe en quelques jours ou semaines. À vrai dire, les mécanismes de cette addiction semblent assez similaires à la dépendance alcoolique. En effet, l'alcool est aussi une molécule GABAergique et il n'est pas étonnant qu'elle ait les mêmes effets que les traitements anxiolytiques : sédation, mais aussi désinhibition, comportement violent et impulsif, addiction et autres. Si un anxiolytique/hypnotique est pris à très forte dose durant longtemps, on peut observer un véritable syndrome de sevrage de type ''delirium tremens'', similaire à celui observé pour l'alcool. ===Les médicaments GABergiques=== Les '''inhibiteurs de la recapture du GABA''' sont peu nombreux, un seul est actuellement utilisé en 2025 : la Tiagabine. Elle est utilisée comme anti-épileptique principalement. Elle peut en théorie être utilisée comme anxiolytique ou somnifère, mais un problème assez important limite grandement cette utilisation : son utilisation chez des patients non-épileptique augmente la survenue d'une épilepsie. Les '''agonistes du GABA''' sont peu utilisés. Le Gamibetal et la progabide agissent sur les deux récepteurs GABA, et sont utilsiés comme anti-épileptiques. Mais de tels agonistes non-sélectifs du GABA sont assez peu utilisés. Les autres agissent sur le récepteur GABA-B uniquement. Le baclofène est un agoniste du récepteur GABA-B, utilisé comme décontractant musculaire. Il sert notamment dans les paralysies liées à une lésion de la moelle épinière, ou dans la sclérose en plaque, pour des paralysies dites spastiques (muscle contracté en permanence). Après les agonistes et inhibiteurs de la recapture du GABA, nous allons enfin parler des '''modulateurs allostériques des récepteurs GABA'''. Ici, il s'agit d'agonistes allostériques du récepteur GABA-A. Les modulateurs allostériques du récepteur GABA-A regroupent les barbituriques, les benzodiazépines et les non-benzodiazépines. Ils agissent sur les récepteurs GABA-A uniquement, pas les autres récepteurs GABA. Les récepteurs GABA-A ont un site de fixation des barbituriques, un site de fixation des benzodiazépines, un site de fixation de l'éthanol, d'autres pour les neurostéroïdes, etc. [[File:GABAa receptor.gif|centre|vignette|upright=2.0|Sites de fixation des récepteurs GABA-A.]] <noinclude>[[File:Barbituric-acid-structural.svg|vignette|upright=0.5|Acide barbiturique.]]</noinclude> Les premiers médicaments de cette classe sont les '''barbituriques''', des dérivés de l'acide barbiturique, dont la structure chimique est illustrée ci-après. Ceux-ci ont d'abord été utilisés comme anesthésiants, avant d'être utilisés à plus faibles doses comme sédatifs et anti-convulsivants, pour ensuite être abandonnés après la mise sur le marché des benzodiazépines. Outre leur action sur les récepteurs GABA-A, ils agissent sur les récepteurs AMPA et kainate du glutamate. <noinclude>[[File:Benzodiazepine.svg|vignette|upright=0.5|Benzodiadépine.]]</noinclude> De nos jours, les barbituriques ont été remplacés par les '''benzodiazépines'''. Ces derniers sont utilisés comme anxiolytiques, car leur demi-vie est assez longue, à quelques exceptions comme le midazolam. Ils étaient autrefois utilisés comme somnifères, en dépit de leur demi-vie longue (le midazolam avec sa courte demi-vie est une exception). Le zolpidem, le zolpiclone et le zaleplon ne sont pas des benzodiazépines mais ont un effet très similaire. Ils sont appelés des '''non-benzodiazépines'''. Ils se distinguent généralement par une demi-vie courte, plus adaptée pour traiter les troubles du sommeil. Les effets secondaires sont les mêmes que pour les benzodiazépines ==Les bloqueurs des canaux ioniques au sodium== Les '''bloqueurs des canaux ioniques au sodium''' agissent, comme leur nom l'indique, sur les canaux ioniques au sodium. Ils empêchent l'ouverture du canal ionique, ce qui empêche le sodium de rentrer dans les neurones. Cela réduit la fréquence des potentiels d'action, et donc l'activité électrique du cerveau. Ils sont utilisés comme médicaments pour traiter l'épilepsie, une maladie causée par une activité électrique anormale du cerveau, dont les causes et manifestations sont assez diverses, qu'on verra en détail dans le chapitre sur l'activité électrique du cerveau. Ils sont aussi utilisés pour traiter les états maniaques des bipolaires, en complément d'un traitement de fond au lithium. Ils sont aussi utilisés dans les traitement des douleurs dites neuropathiques, à savoir d'origine neurologiques. L'épilepsie peut se manifester de plusieurs manières : les crises d'épilepsie classique (perte de conscience et convulsion) appelées crises tonico-cloniques, des myoclonies (tremblements) , des crises d'absences, de crises atoniques (perte de tonus musculaire), et bien d'autres. Et le type d'épilepsie joue beaucoup dans le choix du traitement. Par exemple, certains traitements sont efficaces sur les crises dites tonico-cloniques et inefficaces sur les crises atoniques, tandis que d'autres ont une efficacité inverse. Comme autre exemple, les crises d'absences sont bien soignées par le valproate de sodium, alors que la carbamazépine les aggrave ! Les bloqueurs des canaux sodium se classent en deux types principaux : ceux qui sont dérivés du GABA, et les autres. Ceux dérivés du GABA portent le nom de '''gabapentinoïdes'''. A noter qu'ils n'ont aucune activité GABAergique, à quelques exceptions. Les gabapentinoïdes agissent sur les canaux sodium dépendants du voltage, mais n'agissent pas sur les récepteurs GABA, ne se transforment pas non plus en GABA ''in vivo'', ne participent pas au métabolisme du GABA, etc. Les plus connues sont la prégabaline, le phenibut, la gabapentine, et quelques autres molécules du même genre. [[File:Gabapentinoid-structures.png|centre|vignette|upright=1.5|Gabapentinoides les plus communs.]] Les autres bloqueurs des canaux sodium sont assez disparates. Les plus connus sont la carbamazépine, le valproate de sodium, ainsi que les hydrantoïnes. <noinclude>[[File:Carbamazepine.svg|vignette|upright=0.5|Carbamazepine]]</noinclude> La ''carbamazépine'' est utilisée pour le traitement des épilepsies partielles, ainsi que des crises tonico-cloniques, mais n'est pas usitée pour les absences et myoclonies, car elle peut les aggraver. Elle est aussi utilisée dans les névralgies trigéminales et le traitement des troubles bipolaires (aussi bien lors des dépressions que des états maniaques). Sa molécule est très similaire aux antidépresseurs tricycliques (imipraminiques), ce qui pourrait expliquer son effet thérapeutique sur les dépressions bipolaires, et qui explique aussi pourquoi la carbamazépine ne doit pas être utilisée en même temps que d'autres anti-dépresseurs. L'''oxcarbazépine'' est une molécule structurellement similaire à la carbamazépine, avec les mêmes effets sur le cerveau. Cependant, son métabolisme est différent. La carbamazépine est métabolisée par le foie en un métabolite appelé époxyde, qui a un effet anticonvulsivant, mais est assez toxique pour l'organisme. En comparaison, l'oxcarbazépine n'est pas dégradée en époxyde, mais en dihydrocarbamazépine, un métabolite nettement moins toxique. <noinclude>[[File:Valproic acid.svg|vignette|upright=0.5|Acide valproïque]]</noinclude> Le ''valproate de sodium'' et ses dérivés (''acide valproïque'') sont utilisés dans les épilepsies partielles, certaines épilepsies généralisées avec absences et/ou myoclonies, et les troubles bipolaires. Chimiquement, il s'agit d'un acide gras. Il bloque les canaux sodium, mais a aussi une action sur les récepteurs GABA et NMDA. Ses effets secondaires sont nombreux, mais celui qui a reçu la plus grande publicité dans le grand public est son effet sur le fœtus : ce médicament entraine des malformations sur les fœtus quand il est consommé par les femmes enceintes. <noinclude>[[File:Phenytoin structure.svg|vignette|upright=0.5|Phénytoïne]]</noinclude> Les hydrantoïnes sont une classe de molécules anti-convulsivantes qui ont une structure chimique similaire. La ''phénytoïne'' est une ancienne molécule, encore utilisée, notamment dans certains cas d'urgence. Il faut dire que cette molécule est éliminée d'une manière non-linéaire, ce qui rend les problèmes de dosages fréquents. Elle bloque, comme les médicaments précédents, les canaux sodium. Des dérivés de la phénytoïne existent, les plus connus étant la méphénytoïne, l'éthotoïne et le phénacémide. Ces dérivés sont moins efficaces que la phénytoïne, mais entraînent moins d'effets secondaires. ==Les cholinergiques== Les médicaments qui agissent sur l'acétylcholine sont nombreux. Ils sont regroupés en deux groupes : les cholinomimétiques et les anticholinergiques Les '''cholinomimétiques''' simulent la présence d'acétylcholine en excès dans les synapses, soit en saturant les synapses d'acétylcholine, soit en agissant en agoniste de l'acétylcholine. En conséquence, ils ont des effets similaires, mais aussi des effets secondaires assez similaires. Les agonistes de l'acétylcholine sont des cholinomimétiques, mais il faut aussi citer les inhibiteurs de l'acétylcholinéstérase, l'enzyme qui dégrade l'acétylcholine. Les '''anticholinergiques''' ont un effet opposé aux cholinomimétiques. Les anticholinergiques actuels sont les antagonistes de l'acétylcholine. Ils empêchent l'acétylcholine d'agir sur les récepteurs. Mais d'autres anticholinergiques sont à l'étude. Par exemple, le vésamicol est un inhibiteur du transporteur vésiculaire de l'acétylcholine. Il empêche l'acétylcholine d'être chargé dans les vésicules synaptiques. Il s'agit d'un médicament expérimental, pas encore commercialisé. {|class="wikitable" |- ! ! Agonistes/antagonistes ! Autres |- ! Cholinomimétiques | Agonistes de l'acétylcholine | Inhibiteurs de l'acétylcholinestérase |- ! Anticholinergiques | Antagonistes de l'acétylcholine | |} Les cholinergiques sont moins utilisés que les autres médicaments du système nerveux en raison de leurs effets secondaires. La raison à cela est qu'ils agissent sur le système nerveux autonome, qui gère de nombreux mécanismes vitaux comme la respiration ou le rythme cardiaque. Leur action sur le système nerveux autonome est complétement à part, ce qui fait que nous en parlerons dans une section ultérieure. De plus, leur action sur le système nerveux autonome est l'opposée des médicaments adrénergiques, ce qui fait qu'il vaut mieux voir les deux ensemble. Pour rappel, il y a deux types de récepteurs à l'acétylcholine : les muscariniques et les nicotiniques. Il existe plusieurs sous-types de récepteurs muscariniques et nicotiniques, qui sont répartis différemment dans le corps. Par exemple, les muscles n'ont que des récepteurs nicotiniques, pas de muscariniques. Les récepteurs muscariniques de sous-type M2 sont présents presque uniquement dans le cœur et les poumons. De même, les récepteurs nicotiniques des muscles et du cerveau ne sont pas exactement les mêmes. Aussi, il n'est pas étonnant que certains cholinergiques soient utilisés uniquement pour leur action sur le cerveau, ou uniquement pour leur action musculaire, etc. Ce qui fait que dans ce qui suit, nous allons voir les différents types de médicaments cholinergiques en fonction de leur usage, pas de leur mode d'action. ===Les cholinergiques à action cérébrale=== Dans le cerveau, les récepteurs présents sont majoritairement les récepteurs muscariniques M1 et M4, et quelques sous-types de récepteurs nicotiniques. L'acétylcholine a de nombreux effets sur le cerveau, mais les mieux connus sont liés à la mémoire et la cognition en général. Mais rien d'exploitable pour des médicaments. On n'a pas encore créé de médicaments qui améliorent la mémoire ou la cognition... Les '''agonistes nicotiniques''' ne sont pas utilisés pour leur action sur la cognition. Par contre, ils servent à se sevrer du tabac. En effet, la nicotine est un agoniste nicotinique comme un autre, et on peut la substituer pour un autre agoniste sans trop de problème. Les patients traités fument donc moins, tout en ayant les actions de la nicotine. Le problème est que les agonistes nicotiniques entrainent une dépendance non négligeable, similaire à celle de la nicotine... Les '''agonistes muscariniques''' ne sont pas utilisés pour leur effet sur le système nerveux. Ils sont à l'étude pour la maladie d'Alzheimer avec des résultats décevants pour le moment. La seule exception est la ''xanoméline'', qui est un agoniste des récepteurs muscariniques, avec une préférence pour les récepteurs M1 et M4. Notons que le récepteur M4 est un auto-récepteur. La xanoméline est utilisée pour traiter la schizophrénie et les troubles psychotiques, depuis le milieu des années 2020. Elle était initialement développée pour traiter Alzheimer et avait une efficacité légère, avec cependant des effets secondaires très importants. Par contre, les tests ont montré que ce médicament calmait les psychoses des patients, la psychose étant assez courante dans les cas avancés d'Alzheimer. Le médicament a lors été mis sur le marché, avec cependant de quoi limiter les effets secondaires. Il est combiné avec du ''trospium chloride'', un antagoniste de l'acétylcholine, qui ne rentre pas dans le cerveau. La xanoméline agit donc dans le cerveau seule, alors que son action est compensée par le tropsium chloride dans le reste du corps. Les '''inhibiteurs de l'acétylcholinéstérase''' réduisent la dégradation de l'acétylcholine dans les synapses, ce qui augmente l'effet de l'acétylcholine sur les neurones post-synaptiques. Certaines molécules de ce type sont utilisées comme gaz de combat : le gaz Sarin, le VX et le Novichok sont dans ce cas. Mais parlons plutôt des molécules utilisées comme médicaments... Leur effet sur la cognition et la mémoire est mineur, absolument pas exploitable en pratique. Ils étaient utilisés dans le traitement symptomatique d'Alzheimer, mais étaient très peu efficaces, pas assez pour avoir le moindre effet visible dans la vie quotidienne. Aussi, ils sont maintenant déremboursés, et de moins en moins prescrits. ===Les cholinergiques à action musculaire=== Pour les muscles, l'acétylcholine déclenche la contraction musculaire. Plus précisément, elle transmet les influx nerveux des neurones moteurs vers les muscles. Il faut savoir que les muscles et les neurones moteurs sont connectés par une synapse particulière, appelée la jonction neuromusculaire. Et l'acétylcholine est le neurotransmetteur présent dans toutes les jonctions neuromusculaires. La conséquence est que les cholinergiques sont utilisés comme décontractants musculaires ou au contraire en cas de faiblesses musculaires spécifiques. Les récepteurs synaptiques présents dans cette jonction neuromusculaire sont tous des récepteurs nicotiniques. En clair, les médicaments à action muscariniques n'ont pas d'action sur les muscles, ils n'ont pas d'effets sur la jonction neuromusculaire. Ils sont donc utilisés exclusivement pour leur action sur le cerveau (xanoméline) ou le système nerveux parasympathique. Les agonistes/antagonistes nicotiniques ont eux une action sur les muscles. Mais ce ne sont pas les seuls médicaments cholinergiques agissant sur les muscles. Les '''antagonistes nicotiniques''' entrainent une décontraction musculaire prolongée, pouvant aller jusqu'à la paralysie si la dose est suffisante. Ils sont aussi appelés "curarisants", d'après le nom du curare qui est en le principal représentant. Pour rappel, le curare est un poison qui paralyse tous les muscles, y compris les muscles respiratoires. Il tue en coupant la respiration, asphyxiant sa victime. Les antagonistes nicotiniques ont le même effet à forte dose, ce qui limite leur utilisation en tant que décontractant musculaire. Par contre, ils sont très utiles lors des anesthésies, vu que l’anesthésiste met le patient sous respirateur artificiel ou circulation extracorporelle. Les '''inhibiteurs de l'acétylcholinéstérase''' sont utilisés pour traiter la myasthénie, une maladie dans laquelle les récepteurs de l'acétylcholine disparaissent, en raison d'une attaque du système immunitaire. Il s'agit d'une maladie auto-immune, contrôlée avec des médicaments immunosuppresseurs, mais aussi avec des traitements symptomatiques dont les inhibiteurs de l'acétylcholinéstérase font partie. Les doses utilisées sont différentes de celles autrefois utilisées pour Alzheimer ou les démences. ==Les médicaments du système nerveux autonome== Le ''système nerveux parasympathique'' contrôle de nombreux processus vitaux, essentiels pour la survie : rythme cardiaque, tension artérielle, respiration, transpiration, digestion, etc. Il est souvent dit que son activation prépare le corps au repos et à la digestion (''rest and digest''). * Il a un effet cardiovasculaire : il réduit le rythme cardiaque, réduit la tension artérielle, ralentit la respiration. Aussi, ne vous étonnez pas si les médicaments qui vont suivre sont surtout utilisés en cardiologie. * Il a un effet sur la digestion en général. Il augmente la sécrétion d'enzymes digestives et accélère la motricité intestinale. En conséquence, les médicaments qui vont suivre causent de la constipation ou de la diarrhée. * Il réduit la transpiration et augmente la salivation. Aussi, ne vous étonnez pas si les médicaments qui vont suivre entrainent une bouche sèche, une réduction de la transpiration, ou au contraire une sur-salivation et de la transpiration. * Enfin, il contrôle les mouvements oculaires, notamment de la pupille et la production de larmes. Les cholinergiques peuvent donc dilater ou contracter la pupille, ce qui est utile en ophtalmologie. Le système nerveux parasympathique est opposé au ''système nerveux sympathique'', qui a l'action inverse. l'adage dit qu'il prépare au combat ou à la fuite. Il augmente le rythme cardiaque, contracte les vaisseaux sanguins, accélère la respiration, fait transpirer. En parallèle, il coupe la digestion et bloque la vessie. Le corps entre ainsi en mode "action" et cible sa dépense énergétique sur la survie, au détriment de la digestion. Une dernière action est qu'il dilate les pupilles pour mieux repérer les menaces. Le système parasympathique utilise l'acétylcholine, alors que le sympathique utilise l'adrénaline et la noradrénaline. Les médicaments cholinergiques et adrénergiques ont donc des effets opposés. Un effet cholinergique est équivalent à un effet anti-adrénergique, et réciproquement un effet anti-cholinergique sera équivalent à un effet adrénergique. Tout cela amène à distinguer plusieurs classes de médicaments : * Les cholinergiques stimulent le système parasympathique : ce sont des '''parasympathomimétiques'''. * Les anti-cholinergiques inhibent le système parasympathique : ce sont des '''parasympatholytiques'''. * Les adrénergiques stimulent le système sympathique : ce sont des '''sympathomimétiques'''. * Les anti-adrénergiques inhibent le système sympathique : ce sont des '''sympatholytiques'''. {|class="wikitable" |+ Même couleur = effets similaires |- ! ! Cholinergiques (muscariniques) ! Adrénergiques |- ! Agonistes | class="f_rouge" | Parasympathomimétiques | class="f_bleu" | Sympathomimétiques |- ! Antagonistes | class="f_bleu" | Parasympatholytiques | class="f_rouge" | Sympatholytiques |} Les cholinergiques et les anti-adrénergiques ont des effets très similaires. Idem pour les anti-cholinergiques et les adrénergiques. Cependant, n'allez pas croire qu'ils ont exactement les mêmes effets. Le système nerveux sympathique et parasympathiques ne sont pas exactement opposés et il y a des divergences sur quelques points de détail. Ils s'opposent dans les grandes lignes, mais quelques détails au niveau de l’accommodation oculaire et quelques autres. : En réalité, les deux systèmes sympathique et parasympathique utilisent l'acétylcholine, mais on verra cela dans les chapitres dédiés au système nerveux autonome. Par contre, seul le système sympathique est sensible à l'adrénaline et la noradrénaline. ===Les parasympathomimétiques=== Les parasympathomimétiques regroupent les inhibiteurs de l'acétylcholinéstérase et les agonistes muscariniques. Ils entrainent une hausse de la tension artérielle, un ralentissement du rythme cardiaque, de l'incontinence, une augmentation des sécrétions (transpiration, salive), des nausées et vomissements, des insomnies, des symptômes psychiatriques assez variés, une dilatation des pupilles, etc. Ils sont surtout utilisés pour traiter le glaucome, des problèmes de vessie, etc. En tout cas, ils ne sont pas utilisés pour leur effet sur le système nerveux. Ils sont à l'étude pour la maladie d'Alzheimer avec des résultats décevants pour le moment. ===Les parasympatholytiques=== Les parasympathomimétiques se résument aux '''antagonistes muscariniques'''. Nous avons déjà vu que les antagonistes nicotiniques paralysent les muscles, ce qui fait qu'ils ne sont utilisés qu'en anesthésie et pas ailleurs. Par contre, les antagonistes muscariniques n'ont pas d'effets sur les muscles, ce qui fait qu'on peut les utiliser hors anesthésie. Les plus utilisés sont l'atropine et la scopolamine. La scopolamine est utilisée comme anti-émétique, notamment pour traiter le mal des transports ou les nausées/vomissements après une opération chirurgicale. En ophtalmologie, l'atropine est utilisée pour dilater les pupilles et bloquer le réflexe d’accommodation oculaire, lors certains examens ou certaines opérations. Ils sont parfois utilisés pour traiter les syndromes parkinsoniens, notamment ceux causés par des médicaments antipsychotiques. Leur action sur le système parasympathique entraine cependant des effets secondaires. La réduction de la salivation induit une bouche sèche. Mais surtout, ils dégradent la cognition et de la mémoire. La prise prolongée d'anti-muscariniques augmenterait drastiquement le risque de démence, notamment d'Alzheimer. À ce propos, de nombreux médicaments antidépresseurs, anti-histaminiques ou antipsychotiques ont des effets anticholinergiques. Ils agissent comme de légers antagonistes de l'acétylcholine et cela explique certains de leurs effets secondaires. Pour ce qui est du risque de démence, si le risque est surtout concentré sur les anti-cholinergiques proprement dit, les antihistaminiques sont aussi concernés. Pour les antidépresseurs, il y a moins d'inquiétude à avoir pour le risque de démence avec ces médicaments, qui ont tendance à avoir un effet neuroprotecteur. ===Les sympatholytiques=== Les '''syampatolytiques''' réduisent l'action du système nerveux sympathique, en réduisant l'action de l'adrénaline et de la noradrénaline. Les plus évidents sont les antagonistes des récepteurs adrénergiques, aussi appelés les '''alpha-bloquants''' et les '''bêta-bloquants'''. La distinction entre les deux est qu'ils agissent sur des récepteurs adrénergiques différents : récepteur alpha-1 pour les premiers, récepteurs bêta pour les seconds. Ils sont souvent utilisés pour traiter les problèmes cardiaques. Leur utilisation en psychiatrie ou en neurologie est beaucoup plus rare et aucune indication fiable n'est à l'ordre du jour. : Il faut noter que les alpha-bloquants agissant sur le récepteur alpha-2 sont des sympatholytiques, comme dit plus haut. Nous parlons ici que des récepteurs alpha-1. Les '''agonistes du récepteur <math>\alpha_2</math>''' sont un peu à part. Pour rappel, ce récepteur est un auto-récepteur inhibiteur, à savoir que son activation réduit l'émission de noradrénaline par le neurone (pré-synaptique). Les agonistes de ce récepteur réduisent l'émission de noradrénaline, alors que les antagonistes l'augmentent. Les agonistes les plus connus sont la Guanfacine et la Clonidine. Ils sont utilisés pour traiter le TDAH, l'hypertension, et parfois pour traiter l'anxiété. Niveau effets secondaires psychiatriques, ils tendent à rendre somnolent et dépressif. ===Les sympathomimétiques=== Les '''sympathomimétiques''' ont le même effet que les parasympatholytiques/anti-cholinergiques, à quelques détails près. Mais ils fonctionnent sur un mécanisme différent des anti-cholinergiques. Ils n'agissent pas sur l'acétylcholine, mais sur l'adrénaline et la noradrénaline. Ils sont surtout utilisés en cardiologie, car le cœur est rempli de récepteurs adrénergiques. Les '''inhibiteurs de la recapture de la noradrénaline''' (IRN) sont actuellement utilisés pour les troubles déficitaires de l'attention avec ou sans hyperactivité, et contre la narcolepsie. Deux IRN sont actuellement utilisés pour traiter les troubles de l'attention : l'atomoxetine et la Viloxazine. Ils sont moins efficaces que les médicaments dopaminergiques, mais ils ont un risque addictif bien plus faible et un profil d'effets secondaire différent. Les autres sympatholytiques devraient vous être familiers. En effet, nous avons déjà vu des médicaments qui agissent sur la noradrénaline dans ce qui précède. Par exemple, les inhibiteurs de la monoamine oxydase augmentent la quantité de noradrénaline/adrénaline dans les synapses, en plus de leur action sur la dopamine et la sérotonine. Idem pour les inhibiteurs sélectifs de la sérotonine et de la noradrénaline, ou les antidépresseurs tricycliques. Cependant, ils sont assez peu utilisés pour leur action sur le système nerveux sympathique. Cette action est plus une source d'effets secondaires qu'autre chose. ==Les opioïdes : l'opium et ses dérivés== Les médicaments dits opioïdes sont des agonistes des récepteurs opioïdes. Leur nom vient de l'opium, le latex produit par une plante d’Amérique du Sud nommée le pavot somnifère. Ce latex contient de la morphine et de la codéine, deux opioïdes majeurs qui ont été les premiers opioïdes identifiés. D'autres molécules ayant la même action pharmacologique ont par la suite été découvertes et ont été regroupées sous le terme d’opioïdes. Certaines sont devenues des drogues, comme la cocaïne et l’héroïne, mais d'autres sont devenues des médicaments importants. Les opioïdes sont analgésiques, sédatifs et addictifs. L'effet analgésique est de loin le plus utilisé dans la pratique médicale, quand le paracétamol et les anti-inflammatoires ne sont pas assez efficaces. Ils peuvent aussi être utilisés comme médicament contre la toux, ce qui fait que la codéine ou le dextrométhorphane sont parfois utilisés comme antitussifs. Mais vu leur potentiel addictif, leur utilisation et leur prescription par les médecins est prudente. Les opioïdes ont aussi un effet manifeste sur l'anxiété : qu'il s'agisse de la codéine, de la morphine, du tramadol ou des drogues dérivées, toutes ont un effet anxiolytique. Mais les opioïdes ne sont pas utilisés comme anxiolytiques ou somnifères par les médecins, en raison de leur effet addictif. L'effet addictif n'est pas leur seul effet indésirable. Les opioïdes entraînent des nausées, vomissements, de la constipation, mais aussi et surtout une dépression respiratoire à forte dose. Les overdoses d’opioïdes entraînent une dépression respiratoire très intense, pouvant être fatale. Par contre, la constipation induite par les opioïdes permet de les utiliser comme anti-diarrhéique. Précisons que les opioïdes utilisés comme anti-diarrhéique ne rentrent pas dans le cerveau, mais agissent seulement sur le reste du corps (pour les connaisseurs : ils ne traversent pas la barrière hémato-encéphalique). Tous les opioïdes déclinés en médicaments agissent sur les récepteurs opioïdes de type mu. Pour ce qui est des effets spécifiques aux récepteurs mu, les opioïdes entraînent une euphorie, une contraction musculaire généralisée, une psychose (délires et hallucinations) à fortes doses. L'origine de cet effet délirant/hallucinogène n'est pas très bien connu. L'euphorie induite par les opioïdes est recherché par les toxicomanes, ce qui fait que de nombreuses drogues sont des dérivés de la morphine. Pour ce qui est des autres récepteurs opioïdes, l'action exacte dépend de l'opioïde. Par exemple, la morphine se lie aux récepteurs mu, mais semble avoir peu d'effets sur les autres récepteurs. Une preuve de ce fait est que des souris chez lesquelles on a désactivé le gène qui code ce récepteur mu, et seulement celui-ci, deviennent totalement insensibles à la morphine. Par contre, les souris sans récepteurs kappa et delta réagissent normalement aux opioïdes. Les agonistes du récepteur opioïde Kappa ont eux aussi un effet analgésique, mais entraînent des effets indésirables. Ils entraînent notamment des perturbations du comportement et de l'humeur. Ils ont un effet dépressif, des hallucinations et des délires. Leur effet hallucinatoire est bien plus important que les opioïdes qui agissent seulement sur le récepteur mu. De plus, ils induisent une dépression assez intense, une véritable dysphorie. Cet effet, particulièrement désagréable, limite son usage récréatif ou toxicomaniaque. Tout cela limite fortement leur utilité thérapeutique. ==Les cannabinoïdes : cannabis médical et cannabinoïdes de synthèse== Les entreprises pharmaceutiques font actuellement de la recherche sur les cannabinoïdes, des molécules qui agissent sur les récepteurs endocannabinoïdes. Certains sont naturellement présents dans le cannabis, comme le THC ou le cannabidiol, d'autres sont des produits de synthèse. Le nom plus ou moins trompeur de ''cannabis médical'' fait référence à l'utilisation du THC ou du cannabidiol, voire d'un mélange des deux, mais pas d’autre molécule. Les allégations médicales du cannabis thérapeutique sont nombreuses : douleurs chroniques, la dépression, l'anxiété, les troubles du sommeil, la sclérose en plaques, les vomissements liés à la chimiothérapie, et j'en passe. Et comme de coutume, l’enthousiasme sur le sujet risque d'être la source de beaucoup de désillusions. Pour le moment, seules deux indications sont actuellement confirmées par la FDA américaine : certaines formes rares bien précises d'épilepsie (le syndrome de Dravet et le syndrome de Lennox-Gastaut), et la spasticité liée à la sclérose en plaque (pour laquelle les résultats ne sont pas fameux). Et ce n'est pas faute d'un manque de recherche : les études pour la maladie d'Alzheimer, la sclérose latérale amyotrophiques, les traumas crâniens, les AVC, et bien neurologiques, ont été négatives. Outre le cannabis médical, les entreprises pharmaceutiques travaillent sur des cannabinoïdes autres que le THC ou le cannabidiol. Si la plupart de ces molécules sont encore en développement, il faut cependant citer une exception : le ''Rimonabant''. Le Rimonabant était un agoniste inverse des récepteurs cannabinoïdes, commercialisé en Europe en 2006 avant d'être retiré du marché en 2008. Il était utilisé comme médicament anti-obésité, du fait de son effet coupe-faim. Malheureusement, les données de pharmacovigilance ont rapidement montré que ce médicament avait de sérieux effets secondaires psychiatriques. Près de 10% des utilisateurs développaient une dépression et le risque de suicide était d'environ 1%. Les autorités n'ont pas eu d'autre choix que d'interdire ce médicament une fois les données connues. Les cannabinoïdes sont censés avoir un effet anxiolytique assez marqué. Précisons cependant que cet effet peut parfois s'inverser. Une minorité de consommateurs développe un syndrome anxieux soudain, qui dure quelques heures à quelques jours, après une prise de cannabis (souvent, la toute première prise). Dans certains cas grave, cela peut carrément évoluer rapidement en un véritable syndrome paranoïaque, voire une psychose franche. Encore une fois, les traitements de ce genre ne sont pas utilisés pour leurs effets secondaires (syndrome d'hyperémèse cannabique, psychoses, paranoïa, confusion mentale, amnésie, ...). ==Les adénosinergiques : la caféine et ses dérivés== [[image:Methylxanthin_(R1,_R2,_R3).svg|vignette|Squelette moléculaire partagé par les méthylxanthines.]] Les '''méthylxanthines''' regroupent la caféine, la paraxanthine, la théobromine et la théophylline. Elles partagent toutes le même squelette moléculaire, illustré ci-contre. Suivant ce qu'on met à la place des radicaux R1, R2 et R3, on obtient soit de la caféine, soit de la paraxanthine, et ainsi de suite. Les radicaux R1, R2 et R3 peuvent contenir soit un atome d'hydrogène, soit un radical méthyl (CH3). Si aucun ne l'est, on obtient de la xanthine, qui n'est pas une méthylxanthine proprement dite car il n'y a aucun groupe méthyl. Si au moins un des radicaux R1, R2 ou R3 est méthylé, alors on obtient une méthylxanthine. La caféine a tous les radicaux méthylés, alors que les cas intermédiaires donnent de la théobromine, de la théophylline et de la paraxanthine. {| class="wikitable" style="text-align:center;" |- !Composé !R1 !R2 !R3 |- | colspan="4" bgcolor=#F0C300 |'''Xanthine''' |- !Xanthine |H |H |H |- | colspan="4" bgcolor=#F0C300 |'''Méthylxanthines''' |- !Théobromine |H |CH3 |CH3 |- !Théophylline |CH3 |CH3 |H |- !Paraxanthine |CH3 |H |CH3 |- !Caféine |CH3 |CH3 |CH3 |} {|class="wikitable" |- !Xanthine !Théobromine !Théophylline !Paraxanthine !Caféine |- |[[File:Xanthin - Xanthine.svg|160px|Xanthine.]] |[[File:Theobromin - Theobromine.svg|160px|Théobromine.]] |[[File:Theophyllin - Theophylline.svg|160px|Théophylline.]] |[[File:Paraxanthine.svg|160px|Paraxanthine]] |[[File:Caffeina struttura.svg|160px|Caféine.]] |} Les xanthines et méthylxanthines agissent sur les récepteurs purinergiques, à savoir les récepteurs de l'adénosine. Dans le cerveau, les récepteurs purinergiques les plus courants sont ceux dits de type A1 et A2a. Les deux ont une action inhibitrice, qui réduit le métabolisme et l'activité cérébrale. Les xanthines sont des antagonistes de ces récepteurs, ce qui signifie qu'elles ont un effet inverse à l'adénosine. Là où l'adénosine est sédative, les xanthines et méthylxanthines sont au contraire stimulantes. Leur effet exact sur le cerveau est cependant assez mal connu. SI on sait qu'elles agissent sur les récepteurs purinergiques cérébraux, les conséquences de cette activation sont mal connues. Les chercheurs pensent, sur la base d'arguments expérimentaux, que cette activation causerait une augmentation de la libération de glutamate et de dopamine. Les xanthines seraient donc indirectement dopaminergiques (et glutaminergiques), d'où leur effet stimulant. La plus connue est la '''caféine''', qui améliore l'éveil et l'humeur. Chose intéressante, la caféine lutte contre la pression de sommeil. En effet, celle-ci traduit le fait que quelque chose s'accumule dans le cerveau lors de l'éveil, ce quelque chose étant supposé être des substances somnifères. Le candidat idéal pour cette substance est l'adénosine, mais d'autres substances chimiques sont aussi des prétendants à ce titre, comme la prostaglandine D2. Or, la caféine a un effet indirect sur les récepteurs à adénosine, en jouant le rôle d'antagoniste. La caféine ressemble comme deux gouttes d'eau à l'adénosine, et se fixe sur les mêmes récepteurs membranaires : elle empêche l'adénosine d'arriver à bon port et d'avoir son effet somnifère. C'est pour cela que le café est un excitant : il supprime l'action de l'adénosine sur le cerveau. Précisons que le corps s'habitue à la prise de caféine et y devient de moins en moins sensible, il développe une tolérance à la caféine. La raison est que le nombre de récepteurs à l'adénosine à la surface d'un neurone (ou de toute autre cellule qui exprime ces récepteurs) s'adapte à la demande. Plus on consomme de caféine, plus le nombre de récepteurs augmente. Ce faisant, l'adénosine qui avant ne pouvait pas se lier aux récepteurs bloqués par la caféine peut alors se coller à des récepteurs tous neufs, ce qui réduit l'efficacité de la caféine. C'est tout sauf étonnant, car les récepteurs de l'adénosine sont des récepteurs aux protéines G, qui ont presque tous cette particularité. ==Les antagonistes/agonistes des neuropeptides== Dans cette section, nous allons parler des médicaments qui agissent sur les récepteurs des neuropeptides ou sur leur dégradation. Du moins, ceux dont nous n'avons pas parlé plus haut, comme les récepteurs opioïdes. Il y a assez peu de médicaments de ce genre. Il faut dire que leur délivrance est compliquée : les neuropeptides sont des protéines qui sont rapidement dégradées dans l'estomac, leur absorption n'est pas idéale, etc. Et il faut aussi que les peptides traversent la barrière hémato-encéphalique. Cela fait beaucoup de conditions simples à respecter pour de petites molécules, mais pas pour des grosses protéines. Les rares médicaments de ce genre sont les '''antagonistes des récepteurs NK1'''. Pour rappel, le récepteur NK1 est un récepteur sensible à la substance P et aux tachykinines. De nombreux agonistes de ce type ont été testés dans de nombreuses indications, mais la plupart a échoué lors des essais cliniques. La seule action crédible de ces médicaments est leur effet anti-émétique. Il faut dire que la substance P est fortement présente dans le centre du vomissement, l'aire cérébrale qui gère nausées et vomissements. Les plus connus sont l'aprépitant, le tradipitant, le netupitant, le maropitant, le rolapitant et quelques autres. Ils fonctionnent contre la plupart des nausées et vomissements, quelle que soit leur origine. Mais leurs indications sont souvent plus restreintes. L'aprépitant, le rolapitant et le netupitant sont utilisés contre les nausées/vomissements induits par les chimiothérapies et/ou les nausées/vomissements après une opération chirurgicale. Ils sont souvent combinés avec d'autres médicaments dans ces indications. Le tradipitant et le maropitant sont quant à eux utilisés pour le mal des transports. <noinclude> {{NavChapitre | book=Neurosciences | prev=Les récepteurs synaptiques | prevText=Les récepteurs synaptiques | next=La plasticité synaptique | nextText=La plasticité synaptique }}{{autocat}} </noinclude> p7x9o6birslc58z5ogddfntux8b2wqy Mathc initiation/Fichiers h : c61 0 76758 763517 763313 2026-04-12T08:57:31Z Xhungab 23827 763517 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] : [[Mathc initiation/005s| Sommaire]] : {{Partie{{{type|}}}| L'intégrale de flux de surface}} : En analyse vectorielle, on appelle flux d'un champ vectoriel deux quantités scalaires analogues, selon qu'on le calcule à travers une surface ou une courbe. [https://fr.khanacademy.org/math/multivariable-calculus/integrating-multivariable-functions/3d-flux/v/conceputal-understanding-of-flux-in-three-dimensions Khanacademy : conceputal understanding of flux in three dimensions] : . : Copier la bibliothèque dans votre répertoire de travail : * [[Mathc initiation/Fichiers h : c61a1|x_hfile.h ............ Déclaration des fichiers h]] * [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]] * [[Mathc initiation/Fichiers c : c47ca|x_strcp.h ........... Déclaration des structures (points, vecteurs)]] * [[Mathc initiation/Fichiers h : c25a4|x_fxy.h .............. Calculer les dérivées partielles]] * [[Mathc initiation/Fichiers h : c61a5|x_nxy.h .............. Le vecteur normal n]] * [[Mathc initiation/a314|x_nxz.h .............. Le vecteur normal n]] * [[Mathc initiation/Fichiers h : c61a6|x_nyz.h .............. Le vecteur normal n]] * [[Mathc initiation/Fichiers h : c61a7|x_fdxdy.h ............ Calculer l'intégrale de flux en xy]] * [[Mathc initiation/a315|x_fdxdz.h ............ Calculer l'intégrale de flux en xz]] * [[Mathc initiation/Fichiers h : c61a8|x_fdydx.h ............ Calculer l'intégrale de flux en yx]] * [[Mathc initiation/Fichiers h : c61a9|x_fdydz.h ............ Calculer l'intégrale de flux en yz]] * [[Mathc initiation/a312|x_fdzdx.h ............ Calculer l'intégrale de flux en zx]] * [[Mathc initiation/a313|x_fdzdy.h ............ Calculer l'intégrale de flux en zy]] <syntaxhighlight lang="C"> Dans cette version nous utilisons cet algorithme pour l'intégrale. / b / v(y) | | | | F.(-f_xi-f_yj+k) [f_x^2+f_y^2+1]^1/2 dx dy = | | ----------- | | [f_x^2+f_y^2+1]^1/2 | | / a / u(y) Dans la prochaine version nous utiliserons la version simplifiée. / b / v(y) | | | | F.(-f_xi-f_yj+k) dx dy = | | / a / u(y) </syntaxhighlight> les fonctions f : * [[Mathc initiation/Fichiers h : c61fa|f.h]] : . : Exemples d'application : * [[Mathc initiation/Fichiers c : c61ca|c00a.c ............ ex : en xy ]] * [[Mathc initiation/Fichiers c : c61cb|c00b.c ............ ex : en yx ]] * [[Mathc initiation/Fichiers c : c61cc|c00c.c ............ ex : en yz ]] {{AutoCat}} 2ql1f3vzh0d977ezyerw2g3ivzu1kt1 Mathc initiation/Fichiers h : c62 0 76774 763518 763314 2026-04-12T08:59:42Z Xhungab 23827 763518 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/005s| Sommaire]] {{Partie{{{type|}}}| L'intégrale de flux de surface simplifiée }} En analyse vectorielle, on appelle flux d'un champ vectoriel deux quantités scalaires analogues, selon qu'on le calcule à travers une surface ou une courbe. [[https://en.wikipedia.org/wiki/Surface_integral wikipedia]] : . : Copier la bibliothèque dans votre répertoire de travail : * [[Mathc initiation/Fichiers h : c62a1|x_hfile.h ............ Déclaration des fichiers h]] * [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]] * [[Mathc initiation/Fichiers c : c47ca|x_strcp.h ........... Déclaration des structures (points, vecteurs)]] * [[Mathc initiation/Fichiers h : c25a4|x_fxy.h .............. Calculer les dérivées partielles]] * [[Mathc initiation/Fichiers h : c62a7|x_fdxdy.h ............ Calculer l'intégrale de flux en xy]] * [[Mathc initiation/Fichiers h : c62a8|x_fdxdy.h ............ Calculer l'intégrale de flux en yx]] * [[Mathc initiation/Fichiers h : c62a9|x_fdxdy.h ............ Calculer l'intégrale de flux en yz]] <syntaxhighlight lang="C"> Dans cette version nous utiliserons la version simplifiée. / b / v(y) | | | | F.(-f_xi-f_yj+k) dx dy = | | / a / u(y) </syntaxhighlight> les fonctions f : * [[Mathc initiation/Fichiers h : c61fa|f.h]] : . : Exemples d'application : * [[Mathc initiation/Fichiers c : c62ca|c00a.c ............ ex : en xy ]] * [[Mathc initiation/Fichiers c : c62cb|c00b.c ............ ex : en yx ]] * [[Mathc initiation/Fichiers c : c62cc|c00c.c ............ ex : en yz ]] : {{AutoCat}} ckgb8bh8nvb9d9rk3wqc3lrd437iysr Les cartes graphiques/L'antialiasing 0 78165 763487 759763 2026-04-11T17:53:42Z Mewtow 31375 /* Le supersampling */ 763487 wikitext text/x-wiki [[File:Anti-aliasing demo.svg|vignette|upright=0.5|Effet d'escalier sur les lignes.]] L'antialiasing est une technologie qui permet d'adoucir les bords des objets. Le fait est que dans les jeux vidéos, les bords des objets sont souvent pixelisés, ce qui leur donne un effet d'escalier illustré ci-contre. Le filtre d'antialiasing rajoute une sorte de dégradé pour adoucir les bords des lignes. Il existe un grand nombre de techniques d'antialiasing différentes. Toutes ont des avantages et des inconvénients en termes de performances ou de qualité d'image. ==Le ''Super-Sampling Anti-Aliasing''== La plus simple de ces techniques, le SSAA - '''Super Sampling Anti Aliasing''' - calcule l'image à une résolution supérieure, avant de la réduire. Par exemple, pour rendre une image en 1280 × 1024 en antialiasing 4x, la carte graphique calcule une image en 2560 × 2048, avant de la réduire. Si vous regardez les options de vos pilotes de carte graphique, vous verrez qu'il existe plusieurs réglages pour l'antialiasing : 2X, 4X, 8X, etc. Cette option signifie que l'image calculé par la carte graphique contient respectivement 2, 4, ou 8 fois plus de pixels que l'image originale. Cette technique filtre toute l'image, y compris l'intérieur des textures, mais augmente la consommation de mémoire vidéo et de processeur (on calcule 2, 4, 8, 16 fois plus de pixels). ===L'étape de ''downscaling''=== Le rendu de l'image se fait à une résolution 2, 4, 8, 16 fois plus grande. La résolution n'apparait qu'après le rastériseur, et impacte tout le reste du pipeline à sa suite : pixel shaders, unités de textures et ROP. Le rastériseur produit 2, 4, 8, 16 fois plus de pixels, les unités de textures vont 2, 4, 8, 16 fois plus de travail, idem pour les pixels shaders. Par contre, la réduction de l'image s'effectue avec une seconde passe. Pour effectuer la réduction de l'image, le ROP découpe l'image en rectangles de 2, 4, 8, 16 pixels, et « mélange » les pixels pour obtenir une couleur uniforme. Ce « mélange » est généralement une simple moyenne pondérée, mais on peut aussi utiliser des calculs plus compliqués comme une série d'interpolations linéaires similaire à ce qu'on fait pour filtrer des textures. Pour simplifier les explications, nous allons appeler "sous-pixels" les pixels de l'image rendue dans le pipeline, et pixels les pixels de l'image finale écrite dans le ''framebuffer''. On parle aussi de '''''samples''''' au lieu de sous-pixels. {| |[[File:Supersampling.svg|vignette|Supersampling]] |[[File:Supersampling.png|vignette|Supersampling]] |} ===La position des sous-pixels=== Un point important concernant la qualité de l'antialiasing concerne la position des sous-pixels sur l'écran. Comme vous l'avez vu dans le chapitre sur la rastérisation, notre écran peut être vu comme une sorte de carré, dans lequel on peut repérer des points. Reste que l'on peut placer ces pixels n'importe où sur l'écran, et pas forcément à des positions que les pixels occupent réellement sur l'écran. Pour des pixels, il n'y a aucun intérêt à faire cela, sinon à dégrader l'image. Mais pour des sous-pixels, cela change tout. Toute la problématique peut se résumer en une phrase : où placer nos sous-pixels pour obtenir une meilleure qualité d'image possible. * La solution la plus simple consiste à placer nos sous-pixels à l'endroit qu'ils occuperaient si l'image était réellement rendue avec la résolution simulée par l'antialiasing. Cette solution gère mal les lignes pentues, le pire cas étant les lignes penchées de 45 degrés par rapport à l'horizontale ou la verticale. * Pour mieux gérer les bords penchés, on peut positionner nos sous-pixels comme suit. Les sous-pixels sont placés sur un carré penché (ou sur une ligne si l'on dispose seulement de deux sous-pixels). Des mesures expérimentales montrent que la qualité optimale semble être obtenue avec un angle de rotation d'arctan(1/2) (26,6 degrés), et d'un facteur de rétrécissement de √5/2. * D'autres dispositions sont possibles, notamment une disposition de type Quincunx. <gallery widths="150px" heights="150px"> Supersampling - Uniform.svg|Antialiasing uniforme. Supersampling - RGSS.svg|Antialiasing à grille tournée. Supersampling - Quincunx.svg|Antialiasing Quincunx. </gallery> ==Le multisampling (MSAA)== Le '''Multi-Sampling Anti-Aliasing, abrévié en MSAA''' est une amélioration du SSAA qui économise certains calculs. Pour simplifier, c'est la même chose que le SSAA, sauf que les pixels shaders ne calculent pas l'image à une résolution supérieure, alors que tout le reste (rastérisation, ROP) le fait. Avec le MSAA, l'image à afficher est rendue dans une résolution supérieure, mais les fragments sont regroupés en carrés qui correspondent à un pixel. L'application des textures se fait par pixel et non pour chaque sous-pixel, de même que le pixel shader manipule des pixels, mais ne traite pas les sous-pixels. Avec le SSAA, chaque sous-pixel se verrait appliquer un morceau de texture et un pixel shader, alors qu'on applique la texture sur un pixel complet avec le MSAA. Le calcul de la couleur finale du pixel se fait dans le ROP. Pour cela, le ROP a besoin d'une information quant à la position des sous-pixels. Le pixel final est associé à un triangle précis par la rastérisation. Cependant, cela ne signifie pas que tous les sous-pixels sont associés à ce triangle. En effet, les sous-pixels ne sont pas à la même place que le pixel dans la résolution inférieure. La position des sous-pixels est une chose dont nous parlerons plus en détail ci-dessous. Toujours est-il que l'étape de rastérisation précise si chaque sous-pixel est associé au triangle du pixel. Il se peut que tous les sous-pixels soient sur le triangle, ce qui est signe qu'on est pile sur l'objet, que les sous-pixels sont tous l'intérieur du triangle. Par contre, si certains sous-pixels sont en dehors, c'est signe que l'on est au bord d'un objet. Par exemple, le sous-pixel le plus à gauche sort du triangle, alors que les autres sont dessus. L'unité de rastérisation calcule un '''masque de couverture''', qui précise, pour chaque pixel, quels sont les sous-pixels qui sont ou non dans le triangle. Si un pixel est composé de N sous-pixels, alors ces N sous-pixels sont numérotés de 0 à N-1 en passant dans l'ordre des aiguilles d'une montre. Le masque est un nombre dont chaque bit est associé à un sous-pixel. Le bit associé est à 1 si le sous-pixel est dans le triangle, 0 sinon. Une fois calculé par l'unité de rastérisation, le masque de couverture est transmis aux pixels shaders. Les pixels shaders peuvent utiliser le masque de couverture pour certaines techniques de rendu, mais ce n'est pas une nécessité. Dans la plupart des cas, les pixels shaders ne font rien avec le masque de couverture et le passent tel quel aux ROP. Le ROP prend la couleur calculée par le pixel shader et le masque de couverture. Si un sous-pixel est complétement dans le triangle, sa couleur est celle de la texture. Si le sous-pixel est en dehors du triangle, sa couleur est mise à zéro. Le ROP fait la moyenne des couleurs des sous-pixels du bloc comme avec le SSAA. La seule différence avec le SSAA, c'est que la couleur du pixel calculée par le pixel shader est juste pondérée par le nombre de sous-pixels dans le triangle. Le résultat est que le MSAA ne filtre pas toute l'image, mais seulement les bords des objets, seuls endroit où l'effet d'escalier se fait sentir. ===Les avantages et inconvénients comparé au SSAA=== Niveau avantages, le MSAA n'utilise qu'un seul filtrage de texture par pixel, et non par sous-pixel comme avec le SSAA, ce qui est un gain en performance notable. Le gain en calculs niveau pixel shader est aussi très important, tant que les techniques de rendu utilisant le masque de couverture ne sont pas utilisées. Le gain est d'autant plus important que la majorité des pixels sont situés en plein dans un triangle, les bords d'un objet ne concernant qu'une minorité de pixels/sous-pixels. Mais ce gain en performance a un revers : la qualité de l'antialiasing est moindre. Par définition, le MSAA ne filtre pas l'intérieur des textures, mais seulement les bords des objets. Un défaut de cette technique est que la texture est plaquée au centre du pixel testé. Or, il se peut que le centre du pixel ne soit pas dans la primitive, ce qui arrive si la primitive ne recouvre qu'une petite partie du pixel. Dans un cas pareil, le pixel n'aurait pas été associé à la primitive sans antialiasing, mais il l'est quand l'antialiasing est activé. Un défaut est donc que la texture est appliquée là où elle ne devrait pas l'être. Le résultat est l'apparition d'artefacts graphiques assez légers, mais visibles sur certaines images. Une solution est d'altérer la position des sous-pixels sur le bord des objets pour qu'ils soient dans la primitive. Les sous-pixels sont alors disposés suivant un motif dit centroïde, où tous les sous-pixels sont déplacés de manière à être dans la primitive. Mais un défaut est que les dérivées, le niveau de détail et d'autres données nécessaires au plaquage de texture sont elles aussi altérées, ce qui peut gêner le filtrage de texture. Un autre problème de l'antialiasing tient dans la gestion des textures transparentes, que nous allons détailler dans la section suivante. ===L'antialiasing sur les textures transparentes=== Pour les textures partiellement transparentes, l’antialiasing de type MSAA ne donne pas de bons résultats. Les textures partiellement transparentes servent à rendre des feuillages, des grillages, ou d'autres objets du genre. Prenons l'exemple d'un grillage. La texture de grillage est posée sur une surface carrée, les portions transparentes de la texture correspondant aux trous du grillage entre les grilles, et les portions opaques au grillage lui-même. Dans ce cas, les portions transparentes sont situées dans l'objet et ne sont pas antialiasées. Pourtant, un grillage ou un feuillage sont l'exemple type d'objets où l'effet d’escalier se manifeste. Le problème est surtout visible sur les textures rendues avec la technique de l'''alpha-testing'', où un pixel shader abandonne le rendu d'un pixel si sa transparence dépasse un certain seuil. Les pixels sont coloriés avec une texture, et les pixels trop transparents ne sont pas rendus, alors que les autres pixels sont rendus normalement, avec ''alpha-blending'' dans les ROP et autres. Tout cela a poussé les fabricants de cartes graphiques à inventer diverses techniques pour appliquer l'antialiasing à l'intérieur des textures transparentes. L'idée la plus simple pour cela est d'appliquer le MSAA sur toute l'image, mais de passer en mode SSAA pour les portions de l'image où on a une texture transparente. Le SSAA n'a pas de problèmes pour filtrer l'intérieur des textures, là où le MSAA ne filtre pas l'intérieur des textures. Cela demande cependant de détecter les textures transparentes au niveau du pixel shader, et de les rendre à plus haute résolution façon SSAA. Cette technique a été utilisée sur les cartes NVIDIA sous le nom de ''transparency adaptive anti-aliasing'' (TAAA) et sur les cartes AMD sous le nom d'''adaptive anti-aliasing''. Une autre méthode est la technique dite d''''''alpha to coverage''''', abrévié ATC. Son principe s'explique assez bien en comparant ce qu'on a avec ou sans ATC. Imaginons qu'un pixel soit colorié avec une texture transparente, sans ATC : le pixel se voit attribuer une composante alpha provenant de la texture transparente et passe le test alpha pour savoir s'il doit être rendu avant ou non. Avec ATC, le pixel shader génère un masque de couverture à partir de la composante alpha de la texture lue. Le masque de couverture ainsi généré est alors utilisé par les ROP et le reste du pipeline pour faire l'antialiasing. Cela garantit que les textures transparentes soient antialiasées. ===Les optimisations du ''multisampling''=== Avec l'antialiasing, l'image est rendue à une résolution supérieure, avant de subir un redimensionnement pour rentrer dans la résolution voulue. Cela a des conséquences sur le ''framebuffer''. Le ''framebuffer'' a la taille nécessaire pour la résolution finale, cela ne change pas. Mais le z-buffer et les autres tampons utilisés par le ROP sont agrandis, afin de rendre l'image de résolution supérieure. De plus, le rendu de l'image intermédiaire à haute résolution se fait dans une sorte de pseudo-''framebuffer'' temporaire. L'antialiasing rend l'image de haute résolution dans ce ''framebuffer'' temporaire, puis la redimensionne pour donner l'image finale dans le ''framebuffer'' final. Si on prend un antialiasing 4x, soit avec 4 fois plus de pixels que la résolution initiale, le z-buffer prend 4 fois plus de place, le ''framebuffer'' temporaire aussi. Évidemment, cela prend beaucoup de mémoire vidéo, sans compter que rendre une image à une résolution supérieure prend beaucoup de bande passante, et diverses optimisations ont été inventées pour limiter la casse. Avec le multisampling, il n'est pas rare que plusieurs sous-pixels aient la même couleur. Autant les pixels situés sur les bords d'un objet/triangle ont tendance à avoir des sous-pixels de couleurs différentes, autant les pixels situés à l'intérieur d'un objet sont de couleur uniforme. Cela permet une certaine forme d'optimisation, qui vise à tenir compte de ce cas particulier. L'idée est de compresser le ''framebuffer'' de manière ne pas mémoriser la couleur de chaque sous-pixel pour un pixel uniforme. Au lieu d'écrire quatre couleurs identiques pour 4 sous-pixels, on écrit une seule fois la couleur pour le pixel entier. Notons cependant qu'il existe un type de GPU pour lesquels ce genre d'optimisation n'est pas nécessaire. Rappelez-vous qu'il existe deux types de GPU : ceux en mode immédiat, sujet de ce cours, et ceux en rendu à tile. Avec ces derniers, l'écran est découpé en tiles qui sont rendues séparément, soit l'une après l'autre, soit en parallèle. Le traitement d'une tile fait que l'on n'a pas besoin d'un z-buffer pour toute l'image, mais d'un z-buffer par tile. Même chose pour le ''framebuffer'' temporaire, qui doit mémoriser la tile, pas plus. Les deux sont tellement petits qu'ils peuvent être mémorisés dans une SRAM intégrée au GPU, et non en mémoire vidéo. L'antialiasing est donc réalisé intégralement dans la SRAM intégrée au GPU, sans passer par la mémoire vidéo. ==Le ''Coverage Sampled Anti-Aliasing'' (CSAA) et de ''Enhanced Quality Anti-Aliasing'' (EQAA)== Les techniques de ''multisampling'' précédentes rendaient l’image à une résolution supérieure, sauf dans les pixels shaders et l'étape de plaquage de textures. Mais la résolution supérieure était la même dans tous les pipeline de la carte graphique. Des techniques améliorées partent du même principe que le ''multisampling'', mais changent la résolution suivant les étapes du pipeline. Concrètement, la résolution utilisée par le rastériseur n'est pas la même que dans les pixels shaders/textures, qui elle-même n'est pas la même que dans le z-buffer, qui n'est pas la même que celle du ''framebuffer'' temporaire, etc. C'est le principe des techniques de '''''Coverage Sampled Anti-Aliasing''''' (CSAA) et de '''''Enhanced Quality Anti-Aliasing''''' (EQAA). ===Un nombre de sous-pixel par pixel qui varie suivant l'étape du pipeline=== Au lieu d'utiliser la résolution, nous allons utiliser le nombre de sous-pixels par pixel. Pour le dire autrement, on peut avoir 16 sous-pixels par pixel en sortie du rastériseur, mais 8 sous-pixels par pixel pour le masque de couverture, puis 4 sous-pixels pour le ''z-buffer'' et le ''framebuffer''. Nous allons donner 4 caractéristiques : * le nombre de sous-pixels par pixel en sortie de la rastérisation ; * le nombre de sous-pixels traités par le pixel shader et/ou le plaquage de textures ; * le nombre de sous-pixels par pixel dans le tampon de profondeur ; * le nombre de sous-pixels par pixel dans le ''color buffer'', le ''framebuffer'' temporaire. Ces 5 paramètres seront notés respectivement RSS, SSS, DSS, CSS et CCS. {|class="wikitable" |- ! Mode d'AA ! RSS ! SSS ! DSS ! CSS |- ! Supersampling 8x | 8 || 8 || 8 || 8 |- ! Multisampling 8x | 8 || 1 || 8 || 8 |- ! Coverage Sampled Antialiasing 8x | 8 || 1 || 4 || 4 |- ! Coverage Sampled Antialiasing 16x | 16 || 1 || 4 || 16 |- ! Coverage Sampled Antialiasing 16xQ | 16 || 1 || 8 || 16 |- ! Enhanced Quality Antialiasing 2f4x | 4 || 1 || 2 || 4 |- ! Enhanced Quality Antialiasing 4f8x | 8 || 1 || 4 || 8 |- ! Enhanced Quality Antialiasing 4f16x | 16 || 1 || 4 || 16 |- ! Enhanced Quality Antialiasing 8f16x | 16 || 1 || 8 || 16 |} En général, si on omet l'étape de pixel shading, la résolution diminue au fur et à mesure qu'on progresse dans le pipeline. La résolution est maximale en sortie du rastériseur et elle diminue ou reste constante à chaque étape suivante. Elle reste constante pour le ''multisampling'' pur, mais diminue dans les autres techniques. Ces dernières fusionnent plusieurs sous-pixels rastérisés en plus gros sous-pixels, qui eux sont stockés dans le ''framebuffer'' et le tampon de profondeur. ===La compression du ''framebuffer'' temporaire=== De plus, ces techniques utilisent des techniques de compression similaires à celles utilisées pour les textures sont aussi utilisées. L'idée est simple : il est rare que tous les sous-pixels aient chacun une couleur différente. Prenons par exemple le cas d'un antialiasing 4x, donc un groupe de 4 sous-pixels par pixel : deux sous-pixels vont avoir la même couleur, les deux auront une autre couleur. Dans ce cas, pas besoin de mémoriser 4 couleurs : on a juste à mémoriser deux couleurs et un tableau de 4 bits qui précise quelle pixel a telle couleur (0 pour la première couleur, 1 pour l'autre). On peut adapter la technique avec un nombre plus élevé de sous-pixels et de couleurs. Les techniques de compression les plus simples font que l'on mémorise 2 couleurs par tile de sous-pixels, de la même manière que le font les formats de compression de textures. D'autres techniques peuvent mémoriser 4 couleurs pour 8 sous-pixels, etc. {|class="wikitable" |- ! Mode d'AA ! Nombre de sous-pixels par pixel ! Nombre de couleurs par pixel |- ! Supersampling et Multisampling 8x | 8 || 8 |- ! Coverage Sampled Antialiasing 8x | 8 || 4 |- ! Coverage Sampled Antialiasing 16x | 16 || 4 |- ! Coverage Sampled Antialiasing 8xQ | 8 || 8 |- ! Coverage Sampled Antialiasing 16xQ | 16 || 4 |- ! Enhanced Quality Antialiasing 2f4x | 4 || 2 |- ! Enhanced Quality Antialiasing 4f8x | 8 || 4 |- ! Enhanced Quality Antialiasing 4f16x | 16 || 4 |- ! Enhanced Quality Antialiasing 8f16x | 16 || 8 |} ==L'antialiasing temporel== L''''antialiasing temporel''' (TAA pour ''temporal Anti-Aliasing'') est une technique répartit l'antialiasing sur plusieurs ''frames'', sur plusieurs images. L'idée de l'antialiasing temporel est que chaque image est mélangée avec les images rendues avant elle pour donner un effet d'antialiasing. Mais il ne s'agit pas d'un mélange bête et méchant où chaque image est la moyenne des précédentes. La carte graphique rend chaque image à la même résolution que l'écran, mais chaque image a une position légèrement différente de la précédente, ce qui fait que le mélange de plusieurs images consécutives permet d'affiner la qualité d'image. L'antialiasing temporel subdivise chaque pixel en sous-pixel, sauf qu'au lieu de traiter tous les sous-pixels à chaque image comme le font le ''super-sampling'' et le MSAA, elle ne traite qu'un sous-pixel par pixel à chaque image. Concrètement, si on prend un antialiasing 4x, où chaque pixel est subdivisé en 4 sous-pixels, le premier sous-pixel sera calculé par la première image, le second sous-pixel par la seconde image, etc. Il reste ensuite à appliquer l'opération de mélange sur les 4 images rendues auparavant. Naïvement, on pourrait croire que le filtre de mélange des sous-pixels est effectué toutes les 4 images, mais on peut le faire à chaque rendu d'image en prenant les 4 images précédemment calculées. [[File:TV ghosting interference.jpg|vignette|Exemple de la trainée de mouvement observée avec le TAA avec des objets en mouvement rapide.]] L'avantage du TAA est qu'il est relativement léger en calculs. Le cout est surtout lié au filtre de reconstruction de l'image finale, qui est assez léger en calculs. Cette forme d'antialiasing améliore la qualité de toute l'image, contrairement au MSSA, mais comme le SSAA. Niveau inconvénients, si le TAA marche très bien pour des scènes statiques, il se débrouille assez mal sur les scènes où la caméra bouge vite. Des mouvements trop rapides font que l'image a un flou de mouvement très important, sans compter que les objets en mouvement laissent une sorte de trainée de mouvement visible derrière eux. Notons cependant que le TAA marche d'autant mieux en qualité que le nombre d'images par secondes est élevé. ==L'antialiasing par ''post-processing''== L''''antialiasing par ''post-processing''''' regroupe plusieurs techniques d'antialiasing différentes du SSAA et du MSAA. Avec elles, l'image n'est pas rendue à plus haute résolution avant d'être redimensionnée. A la place, l'image est calculée normalement, à sa résolution finale. Une fois l'image finale mémorisée dans le ''framebuffer'', on lui applique un filtre d'antialiasing spécial. Le filtre en question varie selon la technique utilisée, mais l'idée générale est la même. C'est donc des techniques dites de ''post-processing'', où on calcule l'image, avant de lui faire subir des filtres pour l'embellir. Le filtre en question peut être effectué par les ROP ou par un pixel shader, mais c'est surtout la seconde solution qui est retenue de nos jours. L'algorithme des filtres est généralement assez complexe, ce qui rend sont implémentation en matériel peu pertinente. ===Les avantages et inconvénients=== Contrairement au MSAA, l'antialiasing par ''post-processing'' n'a aucune connaissance de la géométrie de la scène, n'a aucune connaissance des informations données par la rastérisation, n'utilise même pas de sous-pixels. C'est un avantage, car le FXAA filtre la totalité de la scène 3D, même à l'intérieur des textures, et même à l'intérieur des textures transparentes. Par contre, cela peut causer des artefacts graphiques sur certaines portions de l'image. Quand le FXAA est activé, le texte affiché sur une image devient légèrement moins lisible, par exemple. Les techniques de ''post-processing'' ont l'avantage de mieux marcher avec les moteurs de jeux qui utilisent des techniques de rendu différés, dans lesquels une bonne partie des traitements d'éclairage se font sur l'image finale rendue dans le ''framebuffer''. ===Les différentes méthodes d'antialiasing par ''post-processing''=== Le '''''Fast approximate anti-aliasing''''' (FXAA), et le '''''Subpixel Morphological Antialiasing''''' (SMAA) sont les premières techniques d'antialiasing par ''post-processing'' à avoir été intégrées dans les cartes graphiques modernes. Pour le FXAA, le filtre détermine les zones à filtrer en analysant le contraste. Les zones de l'image où le contraste évolue fortement d'un pixel à l'autre sont filtrées, alors que les zones où le contraste varie peu sont gardées intactes. La raison est que les zones où le contraste varie rapidement sont généralement les bords des objets, ou du moins des zones où l'effet d'escalier se fait sentir. l'algorithme exact est dans le domaine public et on peut le trouver facilement sur le net. Par contre, il est difficile d'en expliquer le fonctionnement, pourquoi il marche, aussi je passe cela sous silence. Les cartes graphiques récentes utilisent des techniques basées sur des réseaux de neurones pour effectuer de l'antialiasing. La première d'entre elle est le '''''Deep learning dynamic super resolution''''' (DLDSR), qui consiste à rendre l'image en plus haute résolution, puis à lui appliquer un filtre pour en réduire la résolution. C'est un peu la même chose que le ''supersampling'', sauf que le ''supersampling'' est réalisé dans les ROP, alors que le DLDSR est effectué avec une phase de rendu complète, suivie par l’exécution d'un shader qui redimensionne l'image. Une technique opposée est le '''''Deep learning super sampling''''' (DLSS), qui rend l'image à une résolution inférieure, mais applqiue un filtre qui redimensionne l'image à une plus haute résolution. La première version utilisait un filtre de ''post-processing'', mais les versions suivantes utilisent de l'antialising temporel. Quoique en soit, toutes les versions de DLSS appliquent une forme d'antialiasing, même si elles ''upscalent'' aussi l'image. {{NavChapitre | book=Les cartes graphiques | prev=Le support matériel du lancer de rayons | prevText=Le support matériel du lancer de rayons | next=Le multi-GPU | nextText=Le multi-GPU }}{{autocat}} mv73jpj2zycqa31vkejig3blgsb5jj9 763488 763487 2026-04-11T17:56:44Z Mewtow 31375 /* Le Super-Sampling Anti-Aliasing */ 763488 wikitext text/x-wiki [[File:Anti-aliasing demo.svg|vignette|upright=0.5|Effet d'escalier sur les lignes.]] L'antialiasing est une technologie qui permet d'adoucir les bords des objets. Le fait est que dans les jeux vidéos, les bords des objets sont souvent pixelisés, ce qui leur donne un effet d'escalier illustré ci-contre. Le filtre d'antialiasing rajoute une sorte de dégradé pour adoucir les bords des lignes. Il existe un grand nombre de techniques d'antialiasing différentes. Toutes ont des avantages et des inconvénients en termes de performances ou de qualité d'image. ==Le ''Super-Sampling Anti-Aliasing''== La plus simple de ces techniques, le SSAA - '''Super Sampling Anti Aliasing''' - calcule l'image à une résolution supérieure, avant de la réduire. Par exemple, pour rendre une image en 1280 × 1024 en antialiasing 4x, la carte graphique calcule une image en 2560 × 2048, avant de la réduire. Cette technique filtre toute l'image, y compris l'intérieur des textures, mais augmente la consommation de mémoire vidéo et de processeur (on calcule 2, 4, 8, 16 fois plus de pixels). Si vous regardez les options de vos pilotes de carte graphique, vous verrez qu'il existe plusieurs réglages pour l'antialiasing : 2X, 4X, 8X, etc. Cette option signifie que l'image calculé par la carte graphique contient respectivement 2, 4, ou 8 fois plus de pixels que l'image originale. ===L'étape de ''downscaling''=== Le rendu de l'image se fait à une résolution 2, 4, 8, 16 fois plus grande. La résolution n'apparait qu'après le rastériseur, et impacte tout le reste du pipeline à sa suite : pixel shaders, unités de textures et ROP. Le rastériseur produit 2, 4, 8, 16 fois plus de pixels, les unités de textures vont 2, 4, 8, 16 fois plus de travail, idem pour les pixels shaders. Par contre, la réduction de l'image s'effectue avec une seconde passe. Pour effectuer la réduction de l'image, le ROP découpe l'image en rectangles de 2, 4, 8, 16 pixels, et « mélange » les pixels pour obtenir une couleur uniforme. Ce « mélange » est généralement une simple moyenne pondérée, mais on peut aussi utiliser des calculs plus compliqués comme une série d'interpolations linéaires similaire à ce qu'on fait pour filtrer des textures. Pour simplifier les explications, nous allons appeler "sous-pixels" les pixels de l'image rendue dans le pipeline, et pixels les pixels de l'image finale écrite dans le ''framebuffer''. On parle aussi de '''''samples''''' au lieu de sous-pixels. {| |[[File:Supersampling.svg|vignette|Supersampling]] |[[File:Supersampling.png|vignette|Supersampling]] |} ===La position des sous-pixels=== Un point important concernant la qualité de l'antialiasing concerne la position des sous-pixels sur l'écran. Comme vous l'avez vu dans le chapitre sur la rastérisation, notre écran peut être vu comme une sorte de carré, dans lequel on peut repérer des points. Reste que l'on peut placer ces pixels n'importe où sur l'écran, et pas forcément à des positions que les pixels occupent réellement sur l'écran. Pour des pixels, il n'y a aucun intérêt à faire cela, sinon à dégrader l'image. Mais pour des sous-pixels, cela change tout. Toute la problématique peut se résumer en une phrase : où placer nos sous-pixels pour obtenir une meilleure qualité d'image possible. * La solution la plus simple consiste à placer nos sous-pixels à l'endroit qu'ils occuperaient si l'image était réellement rendue avec la résolution simulée par l'antialiasing. Cette solution gère mal les lignes pentues, le pire cas étant les lignes penchées de 45 degrés par rapport à l'horizontale ou la verticale. * Pour mieux gérer les bords penchés, on peut positionner nos sous-pixels comme suit. Les sous-pixels sont placés sur un carré penché (ou sur une ligne si l'on dispose seulement de deux sous-pixels). Des mesures expérimentales montrent que la qualité optimale semble être obtenue avec un angle de rotation d'arctan(1/2) (26,6 degrés), et d'un facteur de rétrécissement de √5/2. * D'autres dispositions sont possibles, notamment une disposition de type Quincunx. <gallery widths="150px" heights="150px"> Supersampling - Uniform.svg|Antialiasing uniforme. Supersampling - RGSS.svg|Antialiasing à grille tournée. Supersampling - Quincunx.svg|Antialiasing Quincunx. </gallery> ==Le multisampling (MSAA)== Le '''Multi-Sampling Anti-Aliasing, abrévié en MSAA''' est une amélioration du SSAA qui économise certains calculs. Pour simplifier, c'est la même chose que le SSAA, sauf que les pixels shaders ne calculent pas l'image à une résolution supérieure, alors que tout le reste (rastérisation, ROP) le fait. Avec le MSAA, l'image à afficher est rendue dans une résolution supérieure, mais les fragments sont regroupés en carrés qui correspondent à un pixel. L'application des textures se fait par pixel et non pour chaque sous-pixel, de même que le pixel shader manipule des pixels, mais ne traite pas les sous-pixels. Avec le SSAA, chaque sous-pixel se verrait appliquer un morceau de texture et un pixel shader, alors qu'on applique la texture sur un pixel complet avec le MSAA. Le calcul de la couleur finale du pixel se fait dans le ROP. Pour cela, le ROP a besoin d'une information quant à la position des sous-pixels. Le pixel final est associé à un triangle précis par la rastérisation. Cependant, cela ne signifie pas que tous les sous-pixels sont associés à ce triangle. En effet, les sous-pixels ne sont pas à la même place que le pixel dans la résolution inférieure. La position des sous-pixels est une chose dont nous parlerons plus en détail ci-dessous. Toujours est-il que l'étape de rastérisation précise si chaque sous-pixel est associé au triangle du pixel. Il se peut que tous les sous-pixels soient sur le triangle, ce qui est signe qu'on est pile sur l'objet, que les sous-pixels sont tous l'intérieur du triangle. Par contre, si certains sous-pixels sont en dehors, c'est signe que l'on est au bord d'un objet. Par exemple, le sous-pixel le plus à gauche sort du triangle, alors que les autres sont dessus. L'unité de rastérisation calcule un '''masque de couverture''', qui précise, pour chaque pixel, quels sont les sous-pixels qui sont ou non dans le triangle. Si un pixel est composé de N sous-pixels, alors ces N sous-pixels sont numérotés de 0 à N-1 en passant dans l'ordre des aiguilles d'une montre. Le masque est un nombre dont chaque bit est associé à un sous-pixel. Le bit associé est à 1 si le sous-pixel est dans le triangle, 0 sinon. Une fois calculé par l'unité de rastérisation, le masque de couverture est transmis aux pixels shaders. Les pixels shaders peuvent utiliser le masque de couverture pour certaines techniques de rendu, mais ce n'est pas une nécessité. Dans la plupart des cas, les pixels shaders ne font rien avec le masque de couverture et le passent tel quel aux ROP. Le ROP prend la couleur calculée par le pixel shader et le masque de couverture. Si un sous-pixel est complétement dans le triangle, sa couleur est celle de la texture. Si le sous-pixel est en dehors du triangle, sa couleur est mise à zéro. Le ROP fait la moyenne des couleurs des sous-pixels du bloc comme avec le SSAA. La seule différence avec le SSAA, c'est que la couleur du pixel calculée par le pixel shader est juste pondérée par le nombre de sous-pixels dans le triangle. Le résultat est que le MSAA ne filtre pas toute l'image, mais seulement les bords des objets, seuls endroit où l'effet d'escalier se fait sentir. ===Les avantages et inconvénients comparé au SSAA=== Niveau avantages, le MSAA n'utilise qu'un seul filtrage de texture par pixel, et non par sous-pixel comme avec le SSAA, ce qui est un gain en performance notable. Le gain en calculs niveau pixel shader est aussi très important, tant que les techniques de rendu utilisant le masque de couverture ne sont pas utilisées. Le gain est d'autant plus important que la majorité des pixels sont situés en plein dans un triangle, les bords d'un objet ne concernant qu'une minorité de pixels/sous-pixels. Mais ce gain en performance a un revers : la qualité de l'antialiasing est moindre. Par définition, le MSAA ne filtre pas l'intérieur des textures, mais seulement les bords des objets. Un défaut de cette technique est que la texture est plaquée au centre du pixel testé. Or, il se peut que le centre du pixel ne soit pas dans la primitive, ce qui arrive si la primitive ne recouvre qu'une petite partie du pixel. Dans un cas pareil, le pixel n'aurait pas été associé à la primitive sans antialiasing, mais il l'est quand l'antialiasing est activé. Un défaut est donc que la texture est appliquée là où elle ne devrait pas l'être. Le résultat est l'apparition d'artefacts graphiques assez légers, mais visibles sur certaines images. Une solution est d'altérer la position des sous-pixels sur le bord des objets pour qu'ils soient dans la primitive. Les sous-pixels sont alors disposés suivant un motif dit centroïde, où tous les sous-pixels sont déplacés de manière à être dans la primitive. Mais un défaut est que les dérivées, le niveau de détail et d'autres données nécessaires au plaquage de texture sont elles aussi altérées, ce qui peut gêner le filtrage de texture. Un autre problème de l'antialiasing tient dans la gestion des textures transparentes, que nous allons détailler dans la section suivante. ===L'antialiasing sur les textures transparentes=== Pour les textures partiellement transparentes, l’antialiasing de type MSAA ne donne pas de bons résultats. Les textures partiellement transparentes servent à rendre des feuillages, des grillages, ou d'autres objets du genre. Prenons l'exemple d'un grillage. La texture de grillage est posée sur une surface carrée, les portions transparentes de la texture correspondant aux trous du grillage entre les grilles, et les portions opaques au grillage lui-même. Dans ce cas, les portions transparentes sont situées dans l'objet et ne sont pas antialiasées. Pourtant, un grillage ou un feuillage sont l'exemple type d'objets où l'effet d’escalier se manifeste. Le problème est surtout visible sur les textures rendues avec la technique de l'''alpha-testing'', où un pixel shader abandonne le rendu d'un pixel si sa transparence dépasse un certain seuil. Les pixels sont coloriés avec une texture, et les pixels trop transparents ne sont pas rendus, alors que les autres pixels sont rendus normalement, avec ''alpha-blending'' dans les ROP et autres. Tout cela a poussé les fabricants de cartes graphiques à inventer diverses techniques pour appliquer l'antialiasing à l'intérieur des textures transparentes. L'idée la plus simple pour cela est d'appliquer le MSAA sur toute l'image, mais de passer en mode SSAA pour les portions de l'image où on a une texture transparente. Le SSAA n'a pas de problèmes pour filtrer l'intérieur des textures, là où le MSAA ne filtre pas l'intérieur des textures. Cela demande cependant de détecter les textures transparentes au niveau du pixel shader, et de les rendre à plus haute résolution façon SSAA. Cette technique a été utilisée sur les cartes NVIDIA sous le nom de ''transparency adaptive anti-aliasing'' (TAAA) et sur les cartes AMD sous le nom d'''adaptive anti-aliasing''. Une autre méthode est la technique dite d''''''alpha to coverage''''', abrévié ATC. Son principe s'explique assez bien en comparant ce qu'on a avec ou sans ATC. Imaginons qu'un pixel soit colorié avec une texture transparente, sans ATC : le pixel se voit attribuer une composante alpha provenant de la texture transparente et passe le test alpha pour savoir s'il doit être rendu avant ou non. Avec ATC, le pixel shader génère un masque de couverture à partir de la composante alpha de la texture lue. Le masque de couverture ainsi généré est alors utilisé par les ROP et le reste du pipeline pour faire l'antialiasing. Cela garantit que les textures transparentes soient antialiasées. ===Les optimisations du ''multisampling''=== Avec l'antialiasing, l'image est rendue à une résolution supérieure, avant de subir un redimensionnement pour rentrer dans la résolution voulue. Cela a des conséquences sur le ''framebuffer''. Le ''framebuffer'' a la taille nécessaire pour la résolution finale, cela ne change pas. Mais le z-buffer et les autres tampons utilisés par le ROP sont agrandis, afin de rendre l'image de résolution supérieure. De plus, le rendu de l'image intermédiaire à haute résolution se fait dans une sorte de pseudo-''framebuffer'' temporaire. L'antialiasing rend l'image de haute résolution dans ce ''framebuffer'' temporaire, puis la redimensionne pour donner l'image finale dans le ''framebuffer'' final. Si on prend un antialiasing 4x, soit avec 4 fois plus de pixels que la résolution initiale, le z-buffer prend 4 fois plus de place, le ''framebuffer'' temporaire aussi. Évidemment, cela prend beaucoup de mémoire vidéo, sans compter que rendre une image à une résolution supérieure prend beaucoup de bande passante, et diverses optimisations ont été inventées pour limiter la casse. Avec le multisampling, il n'est pas rare que plusieurs sous-pixels aient la même couleur. Autant les pixels situés sur les bords d'un objet/triangle ont tendance à avoir des sous-pixels de couleurs différentes, autant les pixels situés à l'intérieur d'un objet sont de couleur uniforme. Cela permet une certaine forme d'optimisation, qui vise à tenir compte de ce cas particulier. L'idée est de compresser le ''framebuffer'' de manière ne pas mémoriser la couleur de chaque sous-pixel pour un pixel uniforme. Au lieu d'écrire quatre couleurs identiques pour 4 sous-pixels, on écrit une seule fois la couleur pour le pixel entier. Notons cependant qu'il existe un type de GPU pour lesquels ce genre d'optimisation n'est pas nécessaire. Rappelez-vous qu'il existe deux types de GPU : ceux en mode immédiat, sujet de ce cours, et ceux en rendu à tile. Avec ces derniers, l'écran est découpé en tiles qui sont rendues séparément, soit l'une après l'autre, soit en parallèle. Le traitement d'une tile fait que l'on n'a pas besoin d'un z-buffer pour toute l'image, mais d'un z-buffer par tile. Même chose pour le ''framebuffer'' temporaire, qui doit mémoriser la tile, pas plus. Les deux sont tellement petits qu'ils peuvent être mémorisés dans une SRAM intégrée au GPU, et non en mémoire vidéo. L'antialiasing est donc réalisé intégralement dans la SRAM intégrée au GPU, sans passer par la mémoire vidéo. ==Le ''Coverage Sampled Anti-Aliasing'' (CSAA) et de ''Enhanced Quality Anti-Aliasing'' (EQAA)== Les techniques de ''multisampling'' précédentes rendaient l’image à une résolution supérieure, sauf dans les pixels shaders et l'étape de plaquage de textures. Mais la résolution supérieure était la même dans tous les pipeline de la carte graphique. Des techniques améliorées partent du même principe que le ''multisampling'', mais changent la résolution suivant les étapes du pipeline. Concrètement, la résolution utilisée par le rastériseur n'est pas la même que dans les pixels shaders/textures, qui elle-même n'est pas la même que dans le z-buffer, qui n'est pas la même que celle du ''framebuffer'' temporaire, etc. C'est le principe des techniques de '''''Coverage Sampled Anti-Aliasing''''' (CSAA) et de '''''Enhanced Quality Anti-Aliasing''''' (EQAA). ===Un nombre de sous-pixel par pixel qui varie suivant l'étape du pipeline=== Au lieu d'utiliser la résolution, nous allons utiliser le nombre de sous-pixels par pixel. Pour le dire autrement, on peut avoir 16 sous-pixels par pixel en sortie du rastériseur, mais 8 sous-pixels par pixel pour le masque de couverture, puis 4 sous-pixels pour le ''z-buffer'' et le ''framebuffer''. Nous allons donner 4 caractéristiques : * le nombre de sous-pixels par pixel en sortie de la rastérisation ; * le nombre de sous-pixels traités par le pixel shader et/ou le plaquage de textures ; * le nombre de sous-pixels par pixel dans le tampon de profondeur ; * le nombre de sous-pixels par pixel dans le ''color buffer'', le ''framebuffer'' temporaire. Ces 5 paramètres seront notés respectivement RSS, SSS, DSS, CSS et CCS. {|class="wikitable" |- ! Mode d'AA ! RSS ! SSS ! DSS ! CSS |- ! Supersampling 8x | 8 || 8 || 8 || 8 |- ! Multisampling 8x | 8 || 1 || 8 || 8 |- ! Coverage Sampled Antialiasing 8x | 8 || 1 || 4 || 4 |- ! Coverage Sampled Antialiasing 16x | 16 || 1 || 4 || 16 |- ! Coverage Sampled Antialiasing 16xQ | 16 || 1 || 8 || 16 |- ! Enhanced Quality Antialiasing 2f4x | 4 || 1 || 2 || 4 |- ! Enhanced Quality Antialiasing 4f8x | 8 || 1 || 4 || 8 |- ! Enhanced Quality Antialiasing 4f16x | 16 || 1 || 4 || 16 |- ! Enhanced Quality Antialiasing 8f16x | 16 || 1 || 8 || 16 |} En général, si on omet l'étape de pixel shading, la résolution diminue au fur et à mesure qu'on progresse dans le pipeline. La résolution est maximale en sortie du rastériseur et elle diminue ou reste constante à chaque étape suivante. Elle reste constante pour le ''multisampling'' pur, mais diminue dans les autres techniques. Ces dernières fusionnent plusieurs sous-pixels rastérisés en plus gros sous-pixels, qui eux sont stockés dans le ''framebuffer'' et le tampon de profondeur. ===La compression du ''framebuffer'' temporaire=== De plus, ces techniques utilisent des techniques de compression similaires à celles utilisées pour les textures sont aussi utilisées. L'idée est simple : il est rare que tous les sous-pixels aient chacun une couleur différente. Prenons par exemple le cas d'un antialiasing 4x, donc un groupe de 4 sous-pixels par pixel : deux sous-pixels vont avoir la même couleur, les deux auront une autre couleur. Dans ce cas, pas besoin de mémoriser 4 couleurs : on a juste à mémoriser deux couleurs et un tableau de 4 bits qui précise quelle pixel a telle couleur (0 pour la première couleur, 1 pour l'autre). On peut adapter la technique avec un nombre plus élevé de sous-pixels et de couleurs. Les techniques de compression les plus simples font que l'on mémorise 2 couleurs par tile de sous-pixels, de la même manière que le font les formats de compression de textures. D'autres techniques peuvent mémoriser 4 couleurs pour 8 sous-pixels, etc. {|class="wikitable" |- ! Mode d'AA ! Nombre de sous-pixels par pixel ! Nombre de couleurs par pixel |- ! Supersampling et Multisampling 8x | 8 || 8 |- ! Coverage Sampled Antialiasing 8x | 8 || 4 |- ! Coverage Sampled Antialiasing 16x | 16 || 4 |- ! Coverage Sampled Antialiasing 8xQ | 8 || 8 |- ! Coverage Sampled Antialiasing 16xQ | 16 || 4 |- ! Enhanced Quality Antialiasing 2f4x | 4 || 2 |- ! Enhanced Quality Antialiasing 4f8x | 8 || 4 |- ! Enhanced Quality Antialiasing 4f16x | 16 || 4 |- ! Enhanced Quality Antialiasing 8f16x | 16 || 8 |} ==L'antialiasing temporel== L''''antialiasing temporel''' (TAA pour ''temporal Anti-Aliasing'') est une technique répartit l'antialiasing sur plusieurs ''frames'', sur plusieurs images. L'idée de l'antialiasing temporel est que chaque image est mélangée avec les images rendues avant elle pour donner un effet d'antialiasing. Mais il ne s'agit pas d'un mélange bête et méchant où chaque image est la moyenne des précédentes. La carte graphique rend chaque image à la même résolution que l'écran, mais chaque image a une position légèrement différente de la précédente, ce qui fait que le mélange de plusieurs images consécutives permet d'affiner la qualité d'image. L'antialiasing temporel subdivise chaque pixel en sous-pixel, sauf qu'au lieu de traiter tous les sous-pixels à chaque image comme le font le ''super-sampling'' et le MSAA, elle ne traite qu'un sous-pixel par pixel à chaque image. Concrètement, si on prend un antialiasing 4x, où chaque pixel est subdivisé en 4 sous-pixels, le premier sous-pixel sera calculé par la première image, le second sous-pixel par la seconde image, etc. Il reste ensuite à appliquer l'opération de mélange sur les 4 images rendues auparavant. Naïvement, on pourrait croire que le filtre de mélange des sous-pixels est effectué toutes les 4 images, mais on peut le faire à chaque rendu d'image en prenant les 4 images précédemment calculées. [[File:TV ghosting interference.jpg|vignette|Exemple de la trainée de mouvement observée avec le TAA avec des objets en mouvement rapide.]] L'avantage du TAA est qu'il est relativement léger en calculs. Le cout est surtout lié au filtre de reconstruction de l'image finale, qui est assez léger en calculs. Cette forme d'antialiasing améliore la qualité de toute l'image, contrairement au MSSA, mais comme le SSAA. Niveau inconvénients, si le TAA marche très bien pour des scènes statiques, il se débrouille assez mal sur les scènes où la caméra bouge vite. Des mouvements trop rapides font que l'image a un flou de mouvement très important, sans compter que les objets en mouvement laissent une sorte de trainée de mouvement visible derrière eux. Notons cependant que le TAA marche d'autant mieux en qualité que le nombre d'images par secondes est élevé. ==L'antialiasing par ''post-processing''== L''''antialiasing par ''post-processing''''' regroupe plusieurs techniques d'antialiasing différentes du SSAA et du MSAA. Avec elles, l'image n'est pas rendue à plus haute résolution avant d'être redimensionnée. A la place, l'image est calculée normalement, à sa résolution finale. Une fois l'image finale mémorisée dans le ''framebuffer'', on lui applique un filtre d'antialiasing spécial. Le filtre en question varie selon la technique utilisée, mais l'idée générale est la même. C'est donc des techniques dites de ''post-processing'', où on calcule l'image, avant de lui faire subir des filtres pour l'embellir. Le filtre en question peut être effectué par les ROP ou par un pixel shader, mais c'est surtout la seconde solution qui est retenue de nos jours. L'algorithme des filtres est généralement assez complexe, ce qui rend sont implémentation en matériel peu pertinente. ===Les avantages et inconvénients=== Contrairement au MSAA, l'antialiasing par ''post-processing'' n'a aucune connaissance de la géométrie de la scène, n'a aucune connaissance des informations données par la rastérisation, n'utilise même pas de sous-pixels. C'est un avantage, car le FXAA filtre la totalité de la scène 3D, même à l'intérieur des textures, et même à l'intérieur des textures transparentes. Par contre, cela peut causer des artefacts graphiques sur certaines portions de l'image. Quand le FXAA est activé, le texte affiché sur une image devient légèrement moins lisible, par exemple. Les techniques de ''post-processing'' ont l'avantage de mieux marcher avec les moteurs de jeux qui utilisent des techniques de rendu différés, dans lesquels une bonne partie des traitements d'éclairage se font sur l'image finale rendue dans le ''framebuffer''. ===Les différentes méthodes d'antialiasing par ''post-processing''=== Le '''''Fast approximate anti-aliasing''''' (FXAA), et le '''''Subpixel Morphological Antialiasing''''' (SMAA) sont les premières techniques d'antialiasing par ''post-processing'' à avoir été intégrées dans les cartes graphiques modernes. Pour le FXAA, le filtre détermine les zones à filtrer en analysant le contraste. Les zones de l'image où le contraste évolue fortement d'un pixel à l'autre sont filtrées, alors que les zones où le contraste varie peu sont gardées intactes. La raison est que les zones où le contraste varie rapidement sont généralement les bords des objets, ou du moins des zones où l'effet d'escalier se fait sentir. l'algorithme exact est dans le domaine public et on peut le trouver facilement sur le net. Par contre, il est difficile d'en expliquer le fonctionnement, pourquoi il marche, aussi je passe cela sous silence. Les cartes graphiques récentes utilisent des techniques basées sur des réseaux de neurones pour effectuer de l'antialiasing. La première d'entre elle est le '''''Deep learning dynamic super resolution''''' (DLDSR), qui consiste à rendre l'image en plus haute résolution, puis à lui appliquer un filtre pour en réduire la résolution. C'est un peu la même chose que le ''supersampling'', sauf que le ''supersampling'' est réalisé dans les ROP, alors que le DLDSR est effectué avec une phase de rendu complète, suivie par l’exécution d'un shader qui redimensionne l'image. Une technique opposée est le '''''Deep learning super sampling''''' (DLSS), qui rend l'image à une résolution inférieure, mais applqiue un filtre qui redimensionne l'image à une plus haute résolution. La première version utilisait un filtre de ''post-processing'', mais les versions suivantes utilisent de l'antialising temporel. Quoique en soit, toutes les versions de DLSS appliquent une forme d'antialiasing, même si elles ''upscalent'' aussi l'image. {{NavChapitre | book=Les cartes graphiques | prev=Le support matériel du lancer de rayons | prevText=Le support matériel du lancer de rayons | next=Le multi-GPU | nextText=Le multi-GPU }}{{autocat}} t6m8knu8i8mdc8ulhnk7itmg9qfa0wd Les cartes graphiques/Le rendu d'une scène 3D : concepts de base 0 79234 763454 763425 2026-04-11T15:55:50Z Mewtow 31375 /* Les autres utilisations des shaders */ 763454 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. Les GPU modernes supportent uniquement des triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. Les API 3D de l'époque géraient nativement ces primitives assez diverses. 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. 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.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <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. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|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. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[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).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[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. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, 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 par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. 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. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] 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. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] 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 différence entre l'éclairage par pixel et par sommet 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.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]] |} ==Les ''shaders''== Le rendu graphique a beaucoup évolué avec le temps. Le strict 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=Les cartes graphiques : architecture de base | nextText=Les cartes graphiques : architecture de base }}{{autocat}} 1y4kou6r4i77q7gpr4d2wwql4zcboeh 763470 763454 2026-04-11T16:18:44Z Mewtow 31375 /* Le placage de textures inverse */ 763470 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. Les GPU modernes supportent uniquement des triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. Les API 3D de l'époque géraient nativement ces primitives assez diverses. 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. 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.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <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. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|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. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[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).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[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. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, 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 par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. 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. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] 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. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] 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 différence entre l'éclairage par pixel et par sommet 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.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]] |} ==Les ''shaders''== Le rendu graphique a beaucoup évolué avec le temps. Le strict 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=Les cartes graphiques : architecture de base | nextText=Les cartes graphiques : architecture de base }}{{autocat}} e04ho1xis6pcfebgpddpzdz8vy930zv 763471 763470 2026-04-11T16:33:41Z Mewtow 31375 /* Le placage de textures inverse */ 763471 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. Les GPU modernes supportent uniquement des triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. Les API 3D de l'époque géraient nativement ces primitives assez diverses. 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. 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. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. Cependant, faire cela pose un problème : les objets transparents ne marchent pas ! Et pour comprendre pourquoi, ainsi que comment corriger ce problème, nous allons devoir expliquer la notion de fragment. ===La transparence et les fragments=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Mettons qu'on regarde un objet semi-transparent, qui est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, on ne voit pas l'objet semi-transparent et seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Pour résumer, le calcul de la transparence est une moyenne pondérée. La moyenne est pondérée par la transparence de la couleur des deux pixels. On parle alors d''''''alpha blending'''''. [[File:Texture splatting.png|centre|vignette|upright=2.0|Application de textures.]] Maintenant, qu'en est-il pour ce qui est du rendu 3D ? Le mélange est réalisé pixel par pixel. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Si les objets sont opaques et le fragment le plus proche est sélectionné. Sinon, la couleur final est un mélange de plusieurs fragments non-cachés. Pour résumer, la profondeur des fragments doit être gérée, de même que la transparence, etc. Et c'est justement le rôle de l'étage du pipeline que nous allons voir maintenant. Ces opérations sont réalisées dans un circuit qu'on nomme le Raster Operations Pipeline (ROP), aussi appelé Render Output Target, situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. ==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.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <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. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|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. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[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).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[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. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, 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 par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. 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. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] 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. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] 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 différence entre l'éclairage par pixel et par sommet 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.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]] |} ==Les ''shaders''== Le rendu graphique a beaucoup évolué avec le temps. Le strict 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=Les cartes graphiques : architecture de base | nextText=Les cartes graphiques : architecture de base }}{{autocat}} cizrb1qe9fg131le3ga3rdsfggnvx3f 763472 763471 2026-04-11T16:38:28Z Mewtow 31375 /* La transparence et les fragments */ 763472 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. Les GPU modernes supportent uniquement des triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. Les API 3D de l'époque géraient nativement ces primitives assez diverses. 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. 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. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. Cependant, faire cela pose un problème : les objets transparents ne marchent pas ! Et pour comprendre pourquoi, ainsi que comment corriger ce problème, nous allons devoir expliquer la notion de fragment. ===La transparence et les fragments=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Mettons qu'on regarde un objet semi-transparent, qui est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, on ne voit pas l'objet semi-transparent et seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Elle agit comme un coefficient qui dit comment mélanger la couleur de l'objet transparent et celui de l'objet opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Pour résumer, le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Application de textures.]] Maintenant, qu'en est-il pour ce qui est du rendu 3D ? Le mélange est réalisé pixel par pixel. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Si les objets sont opaques et le fragment le plus proche est sélectionné. Sinon, la couleur final est un mélange de plusieurs fragments non-cachés. Pour résumer, la profondeur des fragments doit être gérée, de même que la transparence, etc. Et c'est justement le rôle de l'étage du pipeline que nous allons voir maintenant. Ces opérations sont réalisées dans un circuit qu'on nomme le Raster Operations Pipeline (ROP), aussi appelé Render Output Target, situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. ==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.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <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. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|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. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[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).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[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. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, 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 par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. 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. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] 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. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] 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 différence entre l'éclairage par pixel et par sommet 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.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]] |} ==Les ''shaders''== Le rendu graphique a beaucoup évolué avec le temps. Le strict 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=Les cartes graphiques : architecture de base | nextText=Les cartes graphiques : architecture de base }}{{autocat}} 5ln4vhq03mi9sfodt63dq984wcn219x 763473 763472 2026-04-11T16:39:12Z Mewtow 31375 /* La transparence et les fragments */ 763473 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. Les GPU modernes supportent uniquement des triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. Les API 3D de l'époque géraient nativement ces primitives assez diverses. 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. 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. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. Cependant, faire cela pose un problème : les objets transparents ne marchent pas ! Et pour comprendre pourquoi, ainsi que comment corriger ce problème, nous allons devoir expliquer la notion de fragment. ===La transparence et les fragments=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Mettons qu'on regarde un objet semi-transparent, qui est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, on ne voit pas l'objet semi-transparent et seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Elle agit comme un coefficient qui dit comment mélanger la couleur de l'objet transparent et celui de l'objet opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Pour résumer, le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] Maintenant, qu'en est-il pour ce qui est du rendu 3D ? Le mélange est réalisé pixel par pixel. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Si les objets sont opaques et le fragment le plus proche est sélectionné. Sinon, la couleur final est un mélange de plusieurs fragments non-cachés. Pour résumer, la profondeur des fragments doit être gérée, de même que la transparence, etc. Et c'est justement le rôle de l'étage du pipeline que nous allons voir maintenant. Ces opérations sont réalisées dans un circuit qu'on nomme le Raster Operations Pipeline (ROP), aussi appelé Render Output Target, situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. ==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.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <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. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|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. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[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).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[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. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, 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 par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. 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. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] 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. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] 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 différence entre l'éclairage par pixel et par sommet 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.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]] |} ==Les ''shaders''== Le rendu graphique a beaucoup évolué avec le temps. Le strict 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=Les cartes graphiques : architecture de base | nextText=Les cartes graphiques : architecture de base }}{{autocat}} cqyz71w2wk6lkpf2ultqe77cm5p6mc0 763474 763473 2026-04-11T16:39:36Z Mewtow 31375 /* La transparence et les fragments */ 763474 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. Les GPU modernes supportent uniquement des triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. Les API 3D de l'époque géraient nativement ces primitives assez diverses. 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. 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. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. Cependant, faire cela pose un problème : les objets transparents ne marchent pas ! Et pour comprendre pourquoi, ainsi que comment corriger ce problème, nous allons devoir expliquer la notion de fragment. ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Mettons qu'on regarde un objet semi-transparent, qui est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, on ne voit pas l'objet semi-transparent et seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Elle agit comme un coefficient qui dit comment mélanger la couleur de l'objet transparent et celui de l'objet opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Pour résumer, le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] ===Les fragments=== Maintenant, qu'en est-il pour ce qui est du rendu 3D ? Le mélange est réalisé pixel par pixel. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Si les objets sont opaques et le fragment le plus proche est sélectionné. Sinon, la couleur final est un mélange de plusieurs fragments non-cachés. Pour résumer, la profondeur des fragments doit être gérée, de même que la transparence, etc. Et c'est justement le rôle de l'étage du pipeline que nous allons voir maintenant. Ces opérations sont réalisées dans un circuit qu'on nomme le Raster Operations Pipeline (ROP), aussi appelé Render Output Target, situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. ==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.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <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. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|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. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[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).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[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. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, 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 par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. 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. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] 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. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] 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 différence entre l'éclairage par pixel et par sommet 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.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]] |} ==Les ''shaders''== Le rendu graphique a beaucoup évolué avec le temps. Le strict 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=Les cartes graphiques : architecture de base | nextText=Les cartes graphiques : architecture de base }}{{autocat}} itee1zx5mkxjyjhgpuzwn11okntzhlr 763475 763474 2026-04-11T16:42:30Z Mewtow 31375 /* Les fragments */ 763475 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. Les GPU modernes supportent uniquement des triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. Les API 3D de l'époque géraient nativement ces primitives assez diverses. 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. 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. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. Cependant, faire cela pose un problème : les objets transparents ne marchent pas ! Et pour comprendre pourquoi, ainsi que comment corriger ce problème, nous allons devoir expliquer la notion de fragment. ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Mettons qu'on regarde un objet semi-transparent, qui est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, on ne voit pas l'objet semi-transparent et seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Elle agit comme un coefficient qui dit comment mélanger la couleur de l'objet transparent et celui de l'objet opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Pour résumer, le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] ===Les fragments et les ROPs=== Maintenant, qu'en est-il pour ce qui est du rendu 3D ? Le mélange est réalisé pixel par pixel. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Si les objets sont opaques et le fragment le plus proche est sélectionné. Sinon, la couleur final est un mélange de plusieurs fragments non-cachés. Il y a donc une interaction entre tampon de profondeur et mélange ''alpha''. Le tampon de profondeur est comme désactivé pour les fragments transparents, il n'est appliqué que sur les objets opaques. Pour appliquer le mélange ''alpha'' correctement, la profondeur des fragments est gérée en même temps que la transparence, par un même circuit. Il est appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. En effet, il faut connaitre la couleur final d'un fragment, pour faire les calculs. Et celle-ci n'est connue qu'en sortie de l'unité de texture, au plus tôt. ==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.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <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. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|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. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[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).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[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. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, 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 par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. 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. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] 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. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] 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 différence entre l'éclairage par pixel et par sommet 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.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]] |} ==Les ''shaders''== Le rendu graphique a beaucoup évolué avec le temps. Le strict 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=Les cartes graphiques : architecture de base | nextText=Les cartes graphiques : architecture de base }}{{autocat}} 22by5k161incfoi52fp0d44nwr1l35t 763476 763475 2026-04-11T16:42:48Z Mewtow 31375 /* La transparence, les fragments et les ROPs */ 763476 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. Les GPU modernes supportent uniquement des triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. Les API 3D de l'époque géraient nativement ces primitives assez diverses. 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. 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. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. Cependant, faire cela pose un problème : les objets transparents ne marchent pas ! Et pour comprendre pourquoi, ainsi que comment corriger ce problème, nous allons devoir expliquer la notion de fragment. ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Mettons qu'on regarde un objet semi-transparent, qui est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, on ne voit pas l'objet semi-transparent et seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Elle agit comme un coefficient qui dit comment mélanger la couleur de l'objet transparent et celui de l'objet opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Pour résumer, le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] ===Les fragments et les ROPs=== Maintenant, qu'en est-il pour ce qui est du rendu 3D ? Le mélange est réalisé pixel par pixel. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Si les objets sont opaques et le fragment le plus proche est sélectionné. Sinon, la couleur final est un mélange de plusieurs fragments non-cachés. Il y a donc une interaction entre tampon de profondeur et mélange ''alpha''. Le tampon de profondeur est comme désactivé pour les fragments transparents, il n'est appliqué que sur les objets opaques. Pour appliquer le mélange ''alpha'' correctement, la profondeur des fragments est gérée en même temps que la transparence, par un même circuit. Il est appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. En effet, il faut connaitre la couleur final d'un fragment, pour faire les calculs. Et celle-ci n'est connue qu'en sortie de l'unité de texture, au plus tôt. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ==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.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <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. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|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. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[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).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[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. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, 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 par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. 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. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] 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. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] 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 différence entre l'éclairage par pixel et par sommet 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.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]] |} ==Les ''shaders''== Le rendu graphique a beaucoup évolué avec le temps. Le strict 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=Les cartes graphiques : architecture de base | nextText=Les cartes graphiques : architecture de base }}{{autocat}} is041djrkh1py2tjc5ry8kobjlqh2ka 763477 763476 2026-04-11T16:42:55Z Mewtow 31375 /* Les fragments et les ROPs */ 763477 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. Les GPU modernes supportent uniquement des triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. Les API 3D de l'époque géraient nativement ces primitives assez diverses. 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. 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. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. Cependant, faire cela pose un problème : les objets transparents ne marchent pas ! Et pour comprendre pourquoi, ainsi que comment corriger ce problème, nous allons devoir expliquer la notion de fragment. ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Mettons qu'on regarde un objet semi-transparent, qui est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, on ne voit pas l'objet semi-transparent et seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Elle agit comme un coefficient qui dit comment mélanger la couleur de l'objet transparent et celui de l'objet opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Pour résumer, le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] ===Les fragments et les ROPs=== Maintenant, qu'en est-il pour ce qui est du rendu 3D ? Le mélange est réalisé pixel par pixel. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Si les objets sont opaques et le fragment le plus proche est sélectionné. Sinon, la couleur final est un mélange de plusieurs fragments non-cachés. Il y a donc une interaction entre tampon de profondeur et mélange ''alpha''. Le tampon de profondeur est comme désactivé pour les fragments transparents, il n'est appliqué que sur les objets opaques. Pour appliquer le mélange ''alpha'' correctement, la profondeur des fragments est gérée en même temps que la transparence, par un même circuit. Il est appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. En effet, il faut connaitre la couleur final d'un fragment, pour faire les calculs. Et celle-ci n'est connue qu'en sortie de l'unité de texture, au plus tôt. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ==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.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <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. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|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. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[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).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[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. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, 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 par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. 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. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] 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. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] 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 différence entre l'éclairage par pixel et par sommet 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.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]] |} ==Les ''shaders''== Le rendu graphique a beaucoup évolué avec le temps. Le strict 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=Les cartes graphiques : architecture de base | nextText=Les cartes graphiques : architecture de base }}{{autocat}} 0okkdkars4rcl2v9s2fzbvesxnqfk7b 763478 763477 2026-04-11T16:43:12Z Mewtow 31375 /* La rastérisation */ 763478 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. Les GPU modernes supportent uniquement des triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. Les API 3D de l'époque géraient nativement ces primitives assez diverses. 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. 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 et les textures== 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. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. Cependant, faire cela pose un problème : les objets transparents ne marchent pas ! Et pour comprendre pourquoi, ainsi que comment corriger ce problème, nous allons devoir expliquer la notion de fragment. ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Mettons qu'on regarde un objet semi-transparent, qui est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, on ne voit pas l'objet semi-transparent et seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Elle agit comme un coefficient qui dit comment mélanger la couleur de l'objet transparent et celui de l'objet opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Pour résumer, le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] ===Les fragments et les ROPs=== Maintenant, qu'en est-il pour ce qui est du rendu 3D ? Le mélange est réalisé pixel par pixel. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Si les objets sont opaques et le fragment le plus proche est sélectionné. Sinon, la couleur final est un mélange de plusieurs fragments non-cachés. Il y a donc une interaction entre tampon de profondeur et mélange ''alpha''. Le tampon de profondeur est comme désactivé pour les fragments transparents, il n'est appliqué que sur les objets opaques. Pour appliquer le mélange ''alpha'' correctement, la profondeur des fragments est gérée en même temps que la transparence, par un même circuit. Il est appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. En effet, il faut connaitre la couleur final d'un fragment, pour faire les calculs. Et celle-ci n'est connue qu'en sortie de l'unité de texture, au plus tôt. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ==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.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <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. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|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. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[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).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[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. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, 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 par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. 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. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] 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. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] 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 différence entre l'éclairage par pixel et par sommet 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.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]] |} ==Les ''shaders''== Le rendu graphique a beaucoup évolué avec le temps. Le strict 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=Les cartes graphiques : architecture de base | nextText=Les cartes graphiques : architecture de base }}{{autocat}} cpqfemu91b8nm4c6j84uz29m84cmepo 763479 763478 2026-04-11T16:43:41Z Mewtow 31375 /* La transparence, les fragments et les ROPs */ 763479 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. Les GPU modernes supportent uniquement des triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. Les API 3D de l'époque géraient nativement ces primitives assez diverses. 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. 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 et les textures== 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. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Tampon de profondeur | Placage de textures |} Cependant, faire cela pose un problème : les objets transparents ne marchent pas ! Et pour comprendre pourquoi, ainsi que comment corriger ce problème, nous allons devoir expliquer la notion de fragment. ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Mettons qu'on regarde un objet semi-transparent, qui est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, on ne voit pas l'objet semi-transparent et seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Elle agit comme un coefficient qui dit comment mélanger la couleur de l'objet transparent et celui de l'objet opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Pour résumer, le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] ===Les fragments et les ROPs=== Maintenant, qu'en est-il pour ce qui est du rendu 3D ? Le mélange est réalisé pixel par pixel. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Si les objets sont opaques et le fragment le plus proche est sélectionné. Sinon, la couleur final est un mélange de plusieurs fragments non-cachés. Il y a donc une interaction entre tampon de profondeur et mélange ''alpha''. Le tampon de profondeur est comme désactivé pour les fragments transparents, il n'est appliqué que sur les objets opaques. Pour appliquer le mélange ''alpha'' correctement, la profondeur des fragments est gérée en même temps que la transparence, par un même circuit. Il est appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. En effet, il faut connaitre la couleur final d'un fragment, pour faire les calculs. Et celle-ci n'est connue qu'en sortie de l'unité de texture, au plus tôt. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ==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.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <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. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|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. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[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).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[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. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, 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 par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. 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. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] 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. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] 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 différence entre l'éclairage par pixel et par sommet 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.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]] |} ==Les ''shaders''== Le rendu graphique a beaucoup évolué avec le temps. Le strict 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=Les cartes graphiques : architecture de base | nextText=Les cartes graphiques : architecture de base }}{{autocat}} bqf3d6kn7r95kyj1h7czh6zv3tikrlt 763481 763479 2026-04-11T16:46:33Z Mewtow 31375 /* Les fragments et les ROPs */ 763481 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. Les GPU modernes supportent uniquement des triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. Les API 3D de l'époque géraient nativement ces primitives assez diverses. 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. 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 et les textures== 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. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Tampon de profondeur | Placage de textures |} Cependant, faire cela pose un problème : les objets transparents ne marchent pas ! Et pour comprendre pourquoi, ainsi que comment corriger ce problème, nous allons devoir expliquer la notion de fragment. ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Mettons qu'on regarde un objet semi-transparent, qui est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, on ne voit pas l'objet semi-transparent et seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Elle agit comme un coefficient qui dit comment mélanger la couleur de l'objet transparent et celui de l'objet opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Pour résumer, le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] ===Les fragments et les ROPs=== Maintenant, qu'en est-il pour ce qui est du rendu 3D ? Le mélange est réalisé pixel par pixel. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Il connait notamment une composante ''alpha'', qui est ajouté aux trois couleurs RGB. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Si les objets sont opaques et le fragment le plus proche est sélectionné. Sinon, la couleur final est un mélange de plusieurs fragments non-cachés. Il y a donc une interaction entre tampon de profondeur et mélange ''alpha''. Le tampon de profondeur est comme désactivé pour les fragments transparents, il n'est appliqué que sur les objets opaques. Pour appliquer le mélange ''alpha'' correctement, la profondeur des fragments est gérée en même temps que la transparence, par un même circuit. Il est appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. En effet, il faut connaitre la couleur final d'un fragment, pour faire les calculs. Et celle-ci n'est connue qu'en sortie de l'unité de texture, au plus tôt. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ==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.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <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. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|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. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[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).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[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. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, 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 par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. 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. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] 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. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] 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 différence entre l'éclairage par pixel et par sommet 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.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]] |} ==Les ''shaders''== Le rendu graphique a beaucoup évolué avec le temps. Le strict 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=Les cartes graphiques : architecture de base | nextText=Les cartes graphiques : architecture de base }}{{autocat}} jvaexbj6uww9o22y3fbn0843581rooq 763483 763481 2026-04-11T16:48:55Z Mewtow 31375 /* Les fragments et les ROPs */ 763483 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. Les GPU modernes supportent uniquement des triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. Les API 3D de l'époque géraient nativement ces primitives assez diverses. 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. 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 et les textures== 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. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Tampon de profondeur | Placage de textures |} Cependant, faire cela pose un problème : les objets transparents ne marchent pas ! Et pour comprendre pourquoi, ainsi que comment corriger ce problème, nous allons devoir expliquer la notion de fragment. ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Mettons qu'on regarde un objet semi-transparent, qui est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, on ne voit pas l'objet semi-transparent et seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Elle agit comme un coefficient qui dit comment mélanger la couleur de l'objet transparent et celui de l'objet opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Pour résumer, le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] ===Les fragments et les ROPs=== Maintenant, qu'en est-il pour ce qui est du rendu 3D ? Le mélange est réalisé pixel par pixel. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Il connait notamment une composante ''alpha'', qui est ajouté aux trois couleurs RGB. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Si les objets sont opaques et le fragment le plus proche est sélectionné. Mais avec des fragments transparents, les choses sont plus compliquées, car la couleur final est un mélange de plusieurs fragments non-cachés. Vu que plusieurs fragments sont censés être visibles, on ne sait pas quelle coordonnée z stocker. Il y a donc une interaction entre tampon de profondeur et mélange ''alpha''. Le tampon de profondeur est comme désactivé pour les fragments transparents, il n'est appliqué que sur les objets opaques. Pour appliquer le mélange ''alpha'' correctement, la profondeur des fragments est gérée en même temps que la transparence, par un même circuit. Il est appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. En effet, il faut connaitre la couleur final d'un fragment, pour faire les calculs. Et celle-ci n'est connue qu'en sortie de l'unité de texture, au plus tôt. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ==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.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <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. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|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. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[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).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[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. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, 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 par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. 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. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] 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. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] 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 différence entre l'éclairage par pixel et par sommet 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.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]] |} ==Les ''shaders''== Le rendu graphique a beaucoup évolué avec le temps. Le strict 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=Les cartes graphiques : architecture de base | nextText=Les cartes graphiques : architecture de base }}{{autocat}} qqv7y8qs7j0yqkrz6e3me8jzyd38ikv 763484 763483 2026-04-11T17:42:36Z Mewtow 31375 /* Les shaders */ 763484 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. Les GPU modernes supportent uniquement des triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. Les API 3D de l'époque géraient nativement ces primitives assez diverses. 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. 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 et les textures== 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. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Tampon de profondeur | Placage de textures |} Cependant, faire cela pose un problème : les objets transparents ne marchent pas ! Et pour comprendre pourquoi, ainsi que comment corriger ce problème, nous allons devoir expliquer la notion de fragment. ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Mettons qu'on regarde un objet semi-transparent, qui est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, on ne voit pas l'objet semi-transparent et seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Elle agit comme un coefficient qui dit comment mélanger la couleur de l'objet transparent et celui de l'objet opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Pour résumer, le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] ===Les fragments et les ROPs=== Maintenant, qu'en est-il pour ce qui est du rendu 3D ? Le mélange est réalisé pixel par pixel. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Il connait notamment une composante ''alpha'', qui est ajouté aux trois couleurs RGB. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Si les objets sont opaques et le fragment le plus proche est sélectionné. Mais avec des fragments transparents, les choses sont plus compliquées, car la couleur final est un mélange de plusieurs fragments non-cachés. Vu que plusieurs fragments sont censés être visibles, on ne sait pas quelle coordonnée z stocker. Il y a donc une interaction entre tampon de profondeur et mélange ''alpha''. Le tampon de profondeur est comme désactivé pour les fragments transparents, il n'est appliqué que sur les objets opaques. Pour appliquer le mélange ''alpha'' correctement, la profondeur des fragments est gérée en même temps que la transparence, par un même circuit. Il est appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. En effet, il faut connaitre la couleur final d'un fragment, pour faire les calculs. Et celle-ci n'est connue qu'en sortie de l'unité de texture, au plus tôt. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ==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.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <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. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|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. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[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).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[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. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, 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 par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. 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. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] 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. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] 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 différence entre l'éclairage par pixel et par sommet 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.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]] |} ===Les ''shaders'' : des programmes exécutés sur le GPU=== Maintenant que nous venons de voir les algorithmes d'éclairages, il est temps de voir comment les réaliser sur une carte graphique. Nous venons de voir qu'il y a une différence entre l'éclairage par pixel et par sommet. Intuitivement, l'éclairage par sommet devrait se faire avec les calculs géométriques, alors que l'éclairage par pixel devrait se faire après avoir appliqué les textures. Les toutes premières cartes graphiques ne géraient ni l'éclairage par sommet, ni l'éclairage par pixel. Elles laissaient les calculs géométriques au CPU. Par la suite, la Geforce 256 a intégré '''circuit de ''Transform & Lightning''''', qui s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). Elle gérait alors l'éclairage par sommet, mais un algorithme particulier, qui n'était pas très flexible. Il ne gérait que des ''material'' bien précis (des ''Phong materials''), rien de plus. L'amélioration suivante est venue sur la Geforce 3 : l'unité de T&L est devenue programmable. Au vu le grand nombre d'algorithmes d'éclairages possibles et le grand nombre de ''materials'' possibles, c'était la seule voie possibles. Les programmeurs pouvaient programmer leurs propres algorithmes d'éclairage par sommet, même s'ils devaient aussi programmer les étapes de transformation et de projection. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs. Ce qui est important est que la Geforce 3 a introduit une fonctionnalité absolument cruciale pour le rendu 3D moderne : les '''''shaders'''''. Il s'agit de programmes informatiques exécutés par la carte graphique, qui servaient initialement à coder des algorithmes d'éclairage. D'où leur nom : ''shader'' pour ''shading'' (éclairage en anglais). Il existe plusieurs types de shaders, mais les deux principaux sont les '''''vertex shaders''''' et les '''''pixel shaders'''''. Les pixels shaders s'occupent de l'éclairage par pixel, leur nom est assez parlent. Les vertex shaders s'occupent de l'éclairage par sommet, mais aussi des étapes de transformation/projection. L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Pas besoin de les intégrer dans la carte graphique pour les utiliser, pas besoin d'un circuit distinct pour chaque algorithme. Sans shaders, si la carte graphique ne gère pas un algorithme d'éclairage, on ne peut pas l'utiliser. A la rigueur, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique, avec des performances plus que convenables. 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. {{NavChapitre | book=Les cartes graphiques | prev=Les cartes d'affichage des anciens PC | prevText=Les cartes d'affichage des anciens PC | next=Les cartes graphiques : architecture de base | nextText=Les cartes graphiques : architecture de base }}{{autocat}} ruldd1dvnnl214c2diolrvx43ptd2fk 763485 763484 2026-04-11T17:47:00Z Mewtow 31375 /* Les shaders : des programmes exécutés sur le GPU */ 763485 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. Les GPU modernes supportent uniquement des triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. Les API 3D de l'époque géraient nativement ces primitives assez diverses. 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. 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 et les textures== 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. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Tampon de profondeur | Placage de textures |} Cependant, faire cela pose un problème : les objets transparents ne marchent pas ! Et pour comprendre pourquoi, ainsi que comment corriger ce problème, nous allons devoir expliquer la notion de fragment. ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Mettons qu'on regarde un objet semi-transparent, qui est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, on ne voit pas l'objet semi-transparent et seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Elle agit comme un coefficient qui dit comment mélanger la couleur de l'objet transparent et celui de l'objet opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Pour résumer, le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] ===Les fragments et les ROPs=== Maintenant, qu'en est-il pour ce qui est du rendu 3D ? Le mélange est réalisé pixel par pixel. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Il connait notamment une composante ''alpha'', qui est ajouté aux trois couleurs RGB. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Si les objets sont opaques et le fragment le plus proche est sélectionné. Mais avec des fragments transparents, les choses sont plus compliquées, car la couleur final est un mélange de plusieurs fragments non-cachés. Vu que plusieurs fragments sont censés être visibles, on ne sait pas quelle coordonnée z stocker. Il y a donc une interaction entre tampon de profondeur et mélange ''alpha''. Le tampon de profondeur est comme désactivé pour les fragments transparents, il n'est appliqué que sur les objets opaques. Pour appliquer le mélange ''alpha'' correctement, la profondeur des fragments est gérée en même temps que la transparence, par un même circuit. Il est appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. En effet, il faut connaitre la couleur final d'un fragment, pour faire les calculs. Et celle-ci n'est connue qu'en sortie de l'unité de texture, au plus tôt. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ==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.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <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. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|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. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[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).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[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. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, 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 par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. 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. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] 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. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] 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 différence entre l'éclairage par pixel et par sommet 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.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]] |} ===Les ''shaders'' : des programmes exécutés sur le GPU=== Maintenant que nous venons de voir les algorithmes d'éclairages, il est temps de voir comment les réaliser sur une carte graphique. Nous venons de voir qu'il y a une différence entre l'éclairage par pixel et par sommet. Intuitivement, l'éclairage par sommet devrait se faire avec les calculs géométriques, alors que l'éclairage par pixel devrait se faire après avoir appliqué les textures. Les toutes premières cartes graphiques ne géraient ni l'éclairage par sommet, ni l'éclairage par pixel. Elles laissaient les calculs géométriques au CPU. Par la suite, la Geforce 256 a intégré '''circuit de ''Transform & Lightning''''', qui s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). Elle gérait alors l'éclairage par sommet, mais un algorithme particulier, qui n'était pas très flexible. Il ne gérait que des ''material'' bien précis (des ''Phong materials''), rien de plus. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Unité de T&L : géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} L'amélioration suivante est venue sur la Geforce 3 : l'unité de T&L est devenue programmable. Au vu le grand nombre d'algorithmes d'éclairages possibles et le grand nombre de ''materials'' possibles, c'était la seule voie possibles. Les programmeurs pouvaient programmer leurs propres algorithmes d'éclairage par sommet, même s'ils devaient aussi programmer les étapes de transformation et de projection. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs. Ce qui est important est que la Geforce 3 a introduit une fonctionnalité absolument cruciale pour le rendu 3D moderne : les '''''shaders'''''. Il s'agit de programmes informatiques exécutés par la carte graphique, qui servaient initialement à coder des algorithmes d'éclairage. D'où leur nom : ''shader'' pour ''shading'' (éclairage en anglais). Cependant, l'usage modernes des shaders dépasse le cadre des algorithmes d'éclairage. Il existe plusieurs types de shaders, mais les deux principaux sont les '''''vertex shaders''''' et les '''''pixel shaders'''''. Les pixels shaders s'occupent de l'éclairage par pixel, leur nom est assez parlent. Les vertex shaders s'occupent de l'éclairage par sommet, mais aussi des étapes de transformation/projection. Pour 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. Pour implémenter les shaders, il a fallu ajouter des processeurs à la carte graphique. Les processeurs en question exécutent les shaders, ils peuvent lire ou écrire dans des textures, mais ne font rien d'autres. Les ''vertex shaders'' font tout ce qui a trait à la géométrie, ils remplacent l'unité de T&L. Les pixels shaders sont entre la rastérisation et les ROPs, ils sont très liés à l'unité de texture. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Vertex shader'' | rowspan="2" | Rastérisation | Placage de textures | rowspan="2" |''Raster Operations Pipeline'' |- | class="f_rouge" | ''Pixel shader'' |} L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Pas besoin de les intégrer dans la carte graphique pour les utiliser, pas besoin d'un circuit distinct pour chaque algorithme. Sans shaders, si la carte graphique ne gère pas un algorithme d'éclairage, on ne peut pas l'utiliser. A la rigueur, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique, avec des performances plus que convenables. {{NavChapitre | book=Les cartes graphiques | prev=Les cartes d'affichage des anciens PC | prevText=Les cartes d'affichage des anciens PC | next=Les cartes graphiques : architecture de base | nextText=Les cartes graphiques : architecture de base }}{{autocat}} 67thiu41hf7nj9x7tqqdlfwturiy7dc 763486 763485 2026-04-11T17:47:58Z Mewtow 31375 /* Les shaders : des programmes exécutés sur le GPU */ 763486 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. Les GPU modernes supportent uniquement des triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. Les API 3D de l'époque géraient nativement ces primitives assez diverses. 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. 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 et les textures== 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. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Tampon de profondeur | Placage de textures |} Cependant, faire cela pose un problème : les objets transparents ne marchent pas ! Et pour comprendre pourquoi, ainsi que comment corriger ce problème, nous allons devoir expliquer la notion de fragment. ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Mettons qu'on regarde un objet semi-transparent, qui est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, on ne voit pas l'objet semi-transparent et seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Elle agit comme un coefficient qui dit comment mélanger la couleur de l'objet transparent et celui de l'objet opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Pour résumer, le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] ===Les fragments et les ROPs=== Maintenant, qu'en est-il pour ce qui est du rendu 3D ? Le mélange est réalisé pixel par pixel. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Il connait notamment une composante ''alpha'', qui est ajouté aux trois couleurs RGB. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Si les objets sont opaques et le fragment le plus proche est sélectionné. Mais avec des fragments transparents, les choses sont plus compliquées, car la couleur final est un mélange de plusieurs fragments non-cachés. Vu que plusieurs fragments sont censés être visibles, on ne sait pas quelle coordonnée z stocker. Il y a donc une interaction entre tampon de profondeur et mélange ''alpha''. Le tampon de profondeur est comme désactivé pour les fragments transparents, il n'est appliqué que sur les objets opaques. Pour appliquer le mélange ''alpha'' correctement, la profondeur des fragments est gérée en même temps que la transparence, par un même circuit. Il est appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. En effet, il faut connaitre la couleur final d'un fragment, pour faire les calculs. Et celle-ci n'est connue qu'en sortie de l'unité de texture, au plus tôt. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ==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.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <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. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|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. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[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).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[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. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, 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 par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. 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. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] 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. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] 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 différence entre l'éclairage par pixel et par sommet 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.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]] |} ===Les ''shaders'' : des programmes exécutés sur le GPU=== Maintenant que nous venons de voir les algorithmes d'éclairages, il est temps de voir comment les réaliser sur une carte graphique. Nous venons de voir qu'il y a une différence entre l'éclairage par pixel et par sommet. Intuitivement, l'éclairage par sommet devrait se faire avec les calculs géométriques, alors que l'éclairage par pixel devrait se faire après avoir appliqué les textures. Les toutes premières cartes graphiques ne géraient ni l'éclairage par sommet, ni l'éclairage par pixel. Elles laissaient les calculs géométriques au CPU. Par la suite, la Geforce 256 a intégré '''circuit de ''Transform & Lightning''''', qui s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). Elle gérait alors l'éclairage par sommet, mais un algorithme particulier, qui n'était pas très flexible. Il ne gérait que des ''material'' bien précis (des ''Phong materials''), rien de plus. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Unité de T&L : géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} L'amélioration suivante est venue sur la Geforce 3 : l'unité de T&L est devenue programmable. Au vu le grand nombre d'algorithmes d'éclairages possibles et le grand nombre de ''materials'' possibles, c'était la seule voie possibles. Les programmeurs pouvaient programmer leurs propres algorithmes d'éclairage par sommet, même s'ils devaient aussi programmer les étapes de transformation et de projection. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs. Ce qui est important est que la Geforce 3 a introduit une fonctionnalité absolument cruciale pour le rendu 3D moderne : les '''''shaders'''''. Il s'agit de programmes informatiques exécutés par la carte graphique, qui servaient initialement à coder des algorithmes d'éclairage. D'où leur nom : ''shader'' pour ''shading'' (éclairage en anglais). Cependant, l'usage modernes des shaders dépasse le cadre des algorithmes d'éclairage. L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Pas besoin de les intégrer dans la carte graphique pour les utiliser, pas besoin d'un circuit distinct pour chaque algorithme. Sans shaders, si la carte graphique ne gère pas un algorithme d'éclairage, on ne peut pas l'utiliser. A la rigueur, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique, avec des performances plus que convenables. Il existe plusieurs types de shaders, mais les deux principaux sont les '''''vertex shaders''''' et les '''''pixel shaders'''''. Les pixels shaders s'occupent de l'éclairage par pixel, leur nom est assez parlent. Les vertex shaders s'occupent de l'éclairage par sommet, mais aussi des étapes de transformation/projection. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux. Pour implémenter les shaders, il a fallu ajouter des processeurs à la carte graphique. Les processeurs en question exécutent les shaders, ils peuvent lire ou écrire dans des textures, mais ne font rien d'autres. Les ''vertex shaders'' font tout ce qui a trait à la géométrie, ils remplacent l'unité de T&L. Les pixels shaders sont entre la rastérisation et les ROPs, ils sont très liés à l'unité de texture. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Vertex shader'' | rowspan="2" | Rastérisation | Placage de textures | rowspan="2" |''Raster Operations Pipeline'' |- | class="f_rouge" | ''Pixel shader'' |} {{NavChapitre | book=Les cartes graphiques | prev=Les cartes d'affichage des anciens PC | prevText=Les cartes d'affichage des anciens PC | next=Les cartes graphiques : architecture de base | nextText=Les cartes graphiques : architecture de base }}{{autocat}} 0sfofvdzygq04c2bb7bsaitm34h3u5u 763489 763486 2026-04-11T18:02:11Z Mewtow 31375 /* Les objets 3D et leur géométrie */ 763489 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. Les GPU modernes supportent uniquement des triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie. ===La caméra : le point de vue depuis l'écran=== 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 et les textures== 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. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Tampon de profondeur | Placage de textures |} Cependant, faire cela pose un problème : les objets transparents ne marchent pas ! Et pour comprendre pourquoi, ainsi que comment corriger ce problème, nous allons devoir expliquer la notion de fragment. ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Mettons qu'on regarde un objet semi-transparent, qui est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, on ne voit pas l'objet semi-transparent et seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Elle agit comme un coefficient qui dit comment mélanger la couleur de l'objet transparent et celui de l'objet opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Pour résumer, le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] ===Les fragments et les ROPs=== Maintenant, qu'en est-il pour ce qui est du rendu 3D ? Le mélange est réalisé pixel par pixel. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Il connait notamment une composante ''alpha'', qui est ajouté aux trois couleurs RGB. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Si les objets sont opaques et le fragment le plus proche est sélectionné. Mais avec des fragments transparents, les choses sont plus compliquées, car la couleur final est un mélange de plusieurs fragments non-cachés. Vu que plusieurs fragments sont censés être visibles, on ne sait pas quelle coordonnée z stocker. Il y a donc une interaction entre tampon de profondeur et mélange ''alpha''. Le tampon de profondeur est comme désactivé pour les fragments transparents, il n'est appliqué que sur les objets opaques. Pour appliquer le mélange ''alpha'' correctement, la profondeur des fragments est gérée en même temps que la transparence, par un même circuit. Il est appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. En effet, il faut connaitre la couleur final d'un fragment, pour faire les calculs. Et celle-ci n'est connue qu'en sortie de l'unité de texture, au plus tôt. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ==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.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <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. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|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. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[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).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[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. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, 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 par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. 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. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] 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. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] 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 différence entre l'éclairage par pixel et par sommet 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.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]] |} ===Les ''shaders'' : des programmes exécutés sur le GPU=== Maintenant que nous venons de voir les algorithmes d'éclairages, il est temps de voir comment les réaliser sur une carte graphique. Nous venons de voir qu'il y a une différence entre l'éclairage par pixel et par sommet. Intuitivement, l'éclairage par sommet devrait se faire avec les calculs géométriques, alors que l'éclairage par pixel devrait se faire après avoir appliqué les textures. Les toutes premières cartes graphiques ne géraient ni l'éclairage par sommet, ni l'éclairage par pixel. Elles laissaient les calculs géométriques au CPU. Par la suite, la Geforce 256 a intégré '''circuit de ''Transform & Lightning''''', qui s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). Elle gérait alors l'éclairage par sommet, mais un algorithme particulier, qui n'était pas très flexible. Il ne gérait que des ''material'' bien précis (des ''Phong materials''), rien de plus. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Unité de T&L : géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} L'amélioration suivante est venue sur la Geforce 3 : l'unité de T&L est devenue programmable. Au vu le grand nombre d'algorithmes d'éclairages possibles et le grand nombre de ''materials'' possibles, c'était la seule voie possibles. Les programmeurs pouvaient programmer leurs propres algorithmes d'éclairage par sommet, même s'ils devaient aussi programmer les étapes de transformation et de projection. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs. Ce qui est important est que la Geforce 3 a introduit une fonctionnalité absolument cruciale pour le rendu 3D moderne : les '''''shaders'''''. Il s'agit de programmes informatiques exécutés par la carte graphique, qui servaient initialement à coder des algorithmes d'éclairage. D'où leur nom : ''shader'' pour ''shading'' (éclairage en anglais). Cependant, l'usage modernes des shaders dépasse le cadre des algorithmes d'éclairage. L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Pas besoin de les intégrer dans la carte graphique pour les utiliser, pas besoin d'un circuit distinct pour chaque algorithme. Sans shaders, si la carte graphique ne gère pas un algorithme d'éclairage, on ne peut pas l'utiliser. A la rigueur, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique, avec des performances plus que convenables. Il existe plusieurs types de shaders, mais les deux principaux sont les '''''vertex shaders''''' et les '''''pixel shaders'''''. Les pixels shaders s'occupent de l'éclairage par pixel, leur nom est assez parlent. Les vertex shaders s'occupent de l'éclairage par sommet, mais aussi des étapes de transformation/projection. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux. Pour implémenter les shaders, il a fallu ajouter des processeurs à la carte graphique. Les processeurs en question exécutent les shaders, ils peuvent lire ou écrire dans des textures, mais ne font rien d'autres. Les ''vertex shaders'' font tout ce qui a trait à la géométrie, ils remplacent l'unité de T&L. Les pixels shaders sont entre la rastérisation et les ROPs, ils sont très liés à l'unité de texture. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Vertex shader'' | rowspan="2" | Rastérisation | Placage de textures | rowspan="2" |''Raster Operations Pipeline'' |- | class="f_rouge" | ''Pixel shader'' |} {{NavChapitre | book=Les cartes graphiques | prev=Les cartes d'affichage des anciens PC | prevText=Les cartes d'affichage des anciens PC | next=Les cartes graphiques : architecture de base | nextText=Les cartes graphiques : architecture de base }}{{autocat}} 3nj6ncsf9uwrz048eag9lb9agp14m4h 763490 763489 2026-04-11T18:02:26Z Mewtow 31375 /* Les objets 3D et leur géométrie */ 763490 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.]] 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. Les GPU modernes supportent uniquement des triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie. ===La caméra : le point de vue depuis l'écran=== 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 et les textures== 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. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Tampon de profondeur | Placage de textures |} Cependant, faire cela pose un problème : les objets transparents ne marchent pas ! Et pour comprendre pourquoi, ainsi que comment corriger ce problème, nous allons devoir expliquer la notion de fragment. ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Mettons qu'on regarde un objet semi-transparent, qui est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, on ne voit pas l'objet semi-transparent et seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Elle agit comme un coefficient qui dit comment mélanger la couleur de l'objet transparent et celui de l'objet opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Pour résumer, le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] ===Les fragments et les ROPs=== Maintenant, qu'en est-il pour ce qui est du rendu 3D ? Le mélange est réalisé pixel par pixel. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Il connait notamment une composante ''alpha'', qui est ajouté aux trois couleurs RGB. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Si les objets sont opaques et le fragment le plus proche est sélectionné. Mais avec des fragments transparents, les choses sont plus compliquées, car la couleur final est un mélange de plusieurs fragments non-cachés. Vu que plusieurs fragments sont censés être visibles, on ne sait pas quelle coordonnée z stocker. Il y a donc une interaction entre tampon de profondeur et mélange ''alpha''. Le tampon de profondeur est comme désactivé pour les fragments transparents, il n'est appliqué que sur les objets opaques. Pour appliquer le mélange ''alpha'' correctement, la profondeur des fragments est gérée en même temps que la transparence, par un même circuit. Il est appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. En effet, il faut connaitre la couleur final d'un fragment, pour faire les calculs. Et celle-ci n'est connue qu'en sortie de l'unité de texture, au plus tôt. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ==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.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <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. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|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. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[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).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[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. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, 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 par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. 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. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] 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. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] 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 différence entre l'éclairage par pixel et par sommet 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.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]] |} ===Les ''shaders'' : des programmes exécutés sur le GPU=== Maintenant que nous venons de voir les algorithmes d'éclairages, il est temps de voir comment les réaliser sur une carte graphique. Nous venons de voir qu'il y a une différence entre l'éclairage par pixel et par sommet. Intuitivement, l'éclairage par sommet devrait se faire avec les calculs géométriques, alors que l'éclairage par pixel devrait se faire après avoir appliqué les textures. Les toutes premières cartes graphiques ne géraient ni l'éclairage par sommet, ni l'éclairage par pixel. Elles laissaient les calculs géométriques au CPU. Par la suite, la Geforce 256 a intégré '''circuit de ''Transform & Lightning''''', qui s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). Elle gérait alors l'éclairage par sommet, mais un algorithme particulier, qui n'était pas très flexible. Il ne gérait que des ''material'' bien précis (des ''Phong materials''), rien de plus. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Unité de T&L : géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} L'amélioration suivante est venue sur la Geforce 3 : l'unité de T&L est devenue programmable. Au vu le grand nombre d'algorithmes d'éclairages possibles et le grand nombre de ''materials'' possibles, c'était la seule voie possibles. Les programmeurs pouvaient programmer leurs propres algorithmes d'éclairage par sommet, même s'ils devaient aussi programmer les étapes de transformation et de projection. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs. Ce qui est important est que la Geforce 3 a introduit une fonctionnalité absolument cruciale pour le rendu 3D moderne : les '''''shaders'''''. Il s'agit de programmes informatiques exécutés par la carte graphique, qui servaient initialement à coder des algorithmes d'éclairage. D'où leur nom : ''shader'' pour ''shading'' (éclairage en anglais). Cependant, l'usage modernes des shaders dépasse le cadre des algorithmes d'éclairage. L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Pas besoin de les intégrer dans la carte graphique pour les utiliser, pas besoin d'un circuit distinct pour chaque algorithme. Sans shaders, si la carte graphique ne gère pas un algorithme d'éclairage, on ne peut pas l'utiliser. A la rigueur, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique, avec des performances plus que convenables. Il existe plusieurs types de shaders, mais les deux principaux sont les '''''vertex shaders''''' et les '''''pixel shaders'''''. Les pixels shaders s'occupent de l'éclairage par pixel, leur nom est assez parlent. Les vertex shaders s'occupent de l'éclairage par sommet, mais aussi des étapes de transformation/projection. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux. Pour implémenter les shaders, il a fallu ajouter des processeurs à la carte graphique. Les processeurs en question exécutent les shaders, ils peuvent lire ou écrire dans des textures, mais ne font rien d'autres. Les ''vertex shaders'' font tout ce qui a trait à la géométrie, ils remplacent l'unité de T&L. Les pixels shaders sont entre la rastérisation et les ROPs, ils sont très liés à l'unité de texture. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Vertex shader'' | rowspan="2" | Rastérisation | Placage de textures | rowspan="2" |''Raster Operations Pipeline'' |- | class="f_rouge" | ''Pixel shader'' |} {{NavChapitre | book=Les cartes graphiques | prev=Les cartes d'affichage des anciens PC | prevText=Les cartes d'affichage des anciens PC | next=Les cartes graphiques : architecture de base | nextText=Les cartes graphiques : architecture de base }}{{autocat}} 7j4w3c9qowg7xmiwjirlanp2byzm7bm 763491 763490 2026-04-11T18:03:11Z Mewtow 31375 /* Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel */ 763491 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.]] 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. Les GPU modernes supportent uniquement des triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie. ===La caméra : le point de vue depuis l'écran=== 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 et les textures== 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. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Tampon de profondeur | Placage de textures |} Cependant, faire cela pose un problème : les objets transparents ne marchent pas ! Et pour comprendre pourquoi, ainsi que comment corriger ce problème, nous allons devoir expliquer la notion de fragment. ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Mettons qu'on regarde un objet semi-transparent, qui est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, on ne voit pas l'objet semi-transparent et seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Elle agit comme un coefficient qui dit comment mélanger la couleur de l'objet transparent et celui de l'objet opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Pour résumer, le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] ===Les fragments et les ROPs=== Maintenant, qu'en est-il pour ce qui est du rendu 3D ? Le mélange est réalisé pixel par pixel. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Il connait notamment une composante ''alpha'', qui est ajouté aux trois couleurs RGB. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Si les objets sont opaques et le fragment le plus proche est sélectionné. Mais avec des fragments transparents, les choses sont plus compliquées, car la couleur final est un mélange de plusieurs fragments non-cachés. Vu que plusieurs fragments sont censés être visibles, on ne sait pas quelle coordonnée z stocker. Il y a donc une interaction entre tampon de profondeur et mélange ''alpha''. Le tampon de profondeur est comme désactivé pour les fragments transparents, il n'est appliqué que sur les objets opaques. Pour appliquer le mélange ''alpha'' correctement, la profondeur des fragments est gérée en même temps que la transparence, par un même circuit. Il est appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. En effet, il faut connaitre la couleur final d'un fragment, pour faire les calculs. Et celle-ci n'est connue qu'en sortie de l'unité de texture, au plus tôt. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ==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.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <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. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|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. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[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).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[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. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, 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 par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. 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. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] 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. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] 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 différence entre l'éclairage par pixel et par sommet 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: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]] |} ===Les ''shaders'' : des programmes exécutés sur le GPU=== Maintenant que nous venons de voir les algorithmes d'éclairages, il est temps de voir comment les réaliser sur une carte graphique. Nous venons de voir qu'il y a une différence entre l'éclairage par pixel et par sommet. Intuitivement, l'éclairage par sommet devrait se faire avec les calculs géométriques, alors que l'éclairage par pixel devrait se faire après avoir appliqué les textures. Les toutes premières cartes graphiques ne géraient ni l'éclairage par sommet, ni l'éclairage par pixel. Elles laissaient les calculs géométriques au CPU. Par la suite, la Geforce 256 a intégré '''circuit de ''Transform & Lightning''''', qui s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). Elle gérait alors l'éclairage par sommet, mais un algorithme particulier, qui n'était pas très flexible. Il ne gérait que des ''material'' bien précis (des ''Phong materials''), rien de plus. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Unité de T&L : géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} L'amélioration suivante est venue sur la Geforce 3 : l'unité de T&L est devenue programmable. Au vu le grand nombre d'algorithmes d'éclairages possibles et le grand nombre de ''materials'' possibles, c'était la seule voie possibles. Les programmeurs pouvaient programmer leurs propres algorithmes d'éclairage par sommet, même s'ils devaient aussi programmer les étapes de transformation et de projection. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs. Ce qui est important est que la Geforce 3 a introduit une fonctionnalité absolument cruciale pour le rendu 3D moderne : les '''''shaders'''''. Il s'agit de programmes informatiques exécutés par la carte graphique, qui servaient initialement à coder des algorithmes d'éclairage. D'où leur nom : ''shader'' pour ''shading'' (éclairage en anglais). Cependant, l'usage modernes des shaders dépasse le cadre des algorithmes d'éclairage. L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Pas besoin de les intégrer dans la carte graphique pour les utiliser, pas besoin d'un circuit distinct pour chaque algorithme. Sans shaders, si la carte graphique ne gère pas un algorithme d'éclairage, on ne peut pas l'utiliser. A la rigueur, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique, avec des performances plus que convenables. Il existe plusieurs types de shaders, mais les deux principaux sont les '''''vertex shaders''''' et les '''''pixel shaders'''''. Les pixels shaders s'occupent de l'éclairage par pixel, leur nom est assez parlent. Les vertex shaders s'occupent de l'éclairage par sommet, mais aussi des étapes de transformation/projection. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux. Pour implémenter les shaders, il a fallu ajouter des processeurs à la carte graphique. Les processeurs en question exécutent les shaders, ils peuvent lire ou écrire dans des textures, mais ne font rien d'autres. Les ''vertex shaders'' font tout ce qui a trait à la géométrie, ils remplacent l'unité de T&L. Les pixels shaders sont entre la rastérisation et les ROPs, ils sont très liés à l'unité de texture. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Vertex shader'' | rowspan="2" | Rastérisation | Placage de textures | rowspan="2" |''Raster Operations Pipeline'' |- | class="f_rouge" | ''Pixel shader'' |} {{NavChapitre | book=Les cartes graphiques | prev=Les cartes d'affichage des anciens PC | prevText=Les cartes d'affichage des anciens PC | next=Les cartes graphiques : architecture de base | nextText=Les cartes graphiques : architecture de base }}{{autocat}} 0vi1gz041ys3uk4vkozjir88vj2rbwo 763492 763491 2026-04-11T18:03:42Z Mewtow 31375 763492 wikitext text/x-wiki Le premier jeu à utiliser de la "vraie 3D" texturée fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D. ==Les bases du rendu 3D== 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.]] 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. Les GPU modernes supportent uniquement des triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie. ===La caméra : le point de vue depuis l'écran=== 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 et les textures== 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. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Tampon de profondeur | Placage de textures |} Cependant, faire cela pose un problème : les objets transparents ne marchent pas ! Et pour comprendre pourquoi, ainsi que comment corriger ce problème, nous allons devoir expliquer la notion de fragment. ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Mettons qu'on regarde un objet semi-transparent, qui est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, on ne voit pas l'objet semi-transparent et seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Elle agit comme un coefficient qui dit comment mélanger la couleur de l'objet transparent et celui de l'objet opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Pour résumer, le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] ===Les fragments et les ROPs=== Maintenant, qu'en est-il pour ce qui est du rendu 3D ? Le mélange est réalisé pixel par pixel. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Il connait notamment une composante ''alpha'', qui est ajouté aux trois couleurs RGB. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Si les objets sont opaques et le fragment le plus proche est sélectionné. Mais avec des fragments transparents, les choses sont plus compliquées, car la couleur final est un mélange de plusieurs fragments non-cachés. Vu que plusieurs fragments sont censés être visibles, on ne sait pas quelle coordonnée z stocker. Il y a donc une interaction entre tampon de profondeur et mélange ''alpha''. Le tampon de profondeur est comme désactivé pour les fragments transparents, il n'est appliqué que sur les objets opaques. Pour appliquer le mélange ''alpha'' correctement, la profondeur des fragments est gérée en même temps que la transparence, par un même circuit. Il est appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. En effet, il faut connaitre la couleur final d'un fragment, pour faire les calculs. Et celle-ci n'est connue qu'en sortie de l'unité de texture, au plus tôt. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ==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.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <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. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|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. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[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).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[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. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, 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 par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. 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. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] 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. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] 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 différence entre l'éclairage par pixel et par sommet 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: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]] |} ===Les ''shaders'' : des programmes exécutés sur le GPU=== Maintenant que nous venons de voir les algorithmes d'éclairages, il est temps de voir comment les réaliser sur une carte graphique. Nous venons de voir qu'il y a une différence entre l'éclairage par pixel et par sommet. Intuitivement, l'éclairage par sommet devrait se faire avec les calculs géométriques, alors que l'éclairage par pixel devrait se faire après avoir appliqué les textures. Les toutes premières cartes graphiques ne géraient ni l'éclairage par sommet, ni l'éclairage par pixel. Elles laissaient les calculs géométriques au CPU. Par la suite, la Geforce 256 a intégré '''circuit de ''Transform & Lightning''''', qui s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). Elle gérait alors l'éclairage par sommet, mais un algorithme particulier, qui n'était pas très flexible. Il ne gérait que des ''material'' bien précis (des ''Phong materials''), rien de plus. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Unité de T&L : géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} L'amélioration suivante est venue sur la Geforce 3 : l'unité de T&L est devenue programmable. Au vu le grand nombre d'algorithmes d'éclairages possibles et le grand nombre de ''materials'' possibles, c'était la seule voie possibles. Les programmeurs pouvaient programmer leurs propres algorithmes d'éclairage par sommet, même s'ils devaient aussi programmer les étapes de transformation et de projection. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs. Ce qui est important est que la Geforce 3 a introduit une fonctionnalité absolument cruciale pour le rendu 3D moderne : les '''''shaders'''''. Il s'agit de programmes informatiques exécutés par la carte graphique, qui servaient initialement à coder des algorithmes d'éclairage. D'où leur nom : ''shader'' pour ''shading'' (éclairage en anglais). Cependant, l'usage modernes des shaders dépasse le cadre des algorithmes d'éclairage. L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Pas besoin de les intégrer dans la carte graphique pour les utiliser, pas besoin d'un circuit distinct pour chaque algorithme. Sans shaders, si la carte graphique ne gère pas un algorithme d'éclairage, on ne peut pas l'utiliser. A la rigueur, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique, avec des performances plus que convenables. Il existe plusieurs types de shaders, mais les deux principaux sont les '''''vertex shaders''''' et les '''''pixel shaders'''''. Les pixels shaders s'occupent de l'éclairage par pixel, leur nom est assez parlent. Les vertex shaders s'occupent de l'éclairage par sommet, mais aussi des étapes de transformation/projection. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux. Pour implémenter les shaders, il a fallu ajouter des processeurs à la carte graphique. Les processeurs en question exécutent les shaders, ils peuvent lire ou écrire dans des textures, mais ne font rien d'autres. Les ''vertex shaders'' font tout ce qui a trait à la géométrie, ils remplacent l'unité de T&L. Les pixels shaders sont entre la rastérisation et les ROPs, ils sont très liés à l'unité de texture. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Vertex shader'' | rowspan="2" | Rastérisation | Placage de textures | rowspan="2" |''Raster Operations Pipeline'' |- | class="f_rouge" | ''Pixel shader'' |} {{NavChapitre | book=Les cartes graphiques | prev=Les cartes d'affichage des anciens PC | prevText=Les cartes d'affichage des anciens PC | next=Les cartes graphiques : architecture de base | nextText=Les cartes graphiques : architecture de base }}{{autocat}} 82yv60k051rnbv5d3j9t0yy0eoahqre 763493 763492 2026-04-11T18:05:24Z Mewtow 31375 /* Les objets 3D et leur géométrie */ 763493 wikitext text/x-wiki Le premier jeu à utiliser de la "vraie 3D" texturée fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D. ==Les bases du rendu 3D== 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.]] Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Les sommets sont regroupés en triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie. ===La caméra : le point de vue depuis l'écran=== 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 et les textures== 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. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Tampon de profondeur | Placage de textures |} Cependant, faire cela pose un problème : les objets transparents ne marchent pas ! Et pour comprendre pourquoi, ainsi que comment corriger ce problème, nous allons devoir expliquer la notion de fragment. ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Mettons qu'on regarde un objet semi-transparent, qui est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, on ne voit pas l'objet semi-transparent et seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Elle agit comme un coefficient qui dit comment mélanger la couleur de l'objet transparent et celui de l'objet opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Pour résumer, le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] ===Les fragments et les ROPs=== Maintenant, qu'en est-il pour ce qui est du rendu 3D ? Le mélange est réalisé pixel par pixel. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Il connait notamment une composante ''alpha'', qui est ajouté aux trois couleurs RGB. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Si les objets sont opaques et le fragment le plus proche est sélectionné. Mais avec des fragments transparents, les choses sont plus compliquées, car la couleur final est un mélange de plusieurs fragments non-cachés. Vu que plusieurs fragments sont censés être visibles, on ne sait pas quelle coordonnée z stocker. Il y a donc une interaction entre tampon de profondeur et mélange ''alpha''. Le tampon de profondeur est comme désactivé pour les fragments transparents, il n'est appliqué que sur les objets opaques. Pour appliquer le mélange ''alpha'' correctement, la profondeur des fragments est gérée en même temps que la transparence, par un même circuit. Il est appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. En effet, il faut connaitre la couleur final d'un fragment, pour faire les calculs. Et celle-ci n'est connue qu'en sortie de l'unité de texture, au plus tôt. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ==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.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <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. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|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. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[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).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[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. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, 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 par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. 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. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] 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. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] 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 différence entre l'éclairage par pixel et par sommet 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: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]] |} ===Les ''shaders'' : des programmes exécutés sur le GPU=== Maintenant que nous venons de voir les algorithmes d'éclairages, il est temps de voir comment les réaliser sur une carte graphique. Nous venons de voir qu'il y a une différence entre l'éclairage par pixel et par sommet. Intuitivement, l'éclairage par sommet devrait se faire avec les calculs géométriques, alors que l'éclairage par pixel devrait se faire après avoir appliqué les textures. Les toutes premières cartes graphiques ne géraient ni l'éclairage par sommet, ni l'éclairage par pixel. Elles laissaient les calculs géométriques au CPU. Par la suite, la Geforce 256 a intégré '''circuit de ''Transform & Lightning''''', qui s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). Elle gérait alors l'éclairage par sommet, mais un algorithme particulier, qui n'était pas très flexible. Il ne gérait que des ''material'' bien précis (des ''Phong materials''), rien de plus. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Unité de T&L : géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} L'amélioration suivante est venue sur la Geforce 3 : l'unité de T&L est devenue programmable. Au vu le grand nombre d'algorithmes d'éclairages possibles et le grand nombre de ''materials'' possibles, c'était la seule voie possibles. Les programmeurs pouvaient programmer leurs propres algorithmes d'éclairage par sommet, même s'ils devaient aussi programmer les étapes de transformation et de projection. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs. Ce qui est important est que la Geforce 3 a introduit une fonctionnalité absolument cruciale pour le rendu 3D moderne : les '''''shaders'''''. Il s'agit de programmes informatiques exécutés par la carte graphique, qui servaient initialement à coder des algorithmes d'éclairage. D'où leur nom : ''shader'' pour ''shading'' (éclairage en anglais). Cependant, l'usage modernes des shaders dépasse le cadre des algorithmes d'éclairage. L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Pas besoin de les intégrer dans la carte graphique pour les utiliser, pas besoin d'un circuit distinct pour chaque algorithme. Sans shaders, si la carte graphique ne gère pas un algorithme d'éclairage, on ne peut pas l'utiliser. A la rigueur, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique, avec des performances plus que convenables. Il existe plusieurs types de shaders, mais les deux principaux sont les '''''vertex shaders''''' et les '''''pixel shaders'''''. Les pixels shaders s'occupent de l'éclairage par pixel, leur nom est assez parlent. Les vertex shaders s'occupent de l'éclairage par sommet, mais aussi des étapes de transformation/projection. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux. Pour implémenter les shaders, il a fallu ajouter des processeurs à la carte graphique. Les processeurs en question exécutent les shaders, ils peuvent lire ou écrire dans des textures, mais ne font rien d'autres. Les ''vertex shaders'' font tout ce qui a trait à la géométrie, ils remplacent l'unité de T&L. Les pixels shaders sont entre la rastérisation et les ROPs, ils sont très liés à l'unité de texture. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Vertex shader'' | rowspan="2" | Rastérisation | Placage de textures | rowspan="2" |''Raster Operations Pipeline'' |- | class="f_rouge" | ''Pixel shader'' |} {{NavChapitre | book=Les cartes graphiques | prev=Les cartes d'affichage des anciens PC | prevText=Les cartes d'affichage des anciens PC | next=Les cartes graphiques : architecture de base | nextText=Les cartes graphiques : architecture de base }}{{autocat}} 6f8jvyfmc0t1cvdg5kktwpzetpe2ksw 763494 763493 2026-04-11T18:08:01Z Mewtow 31375 /* Les textures */ 763494 wikitext text/x-wiki Le premier jeu à utiliser de la "vraie 3D" texturée fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D. ==Les bases du rendu 3D== 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.]] Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Les sommets sont regroupés en triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie. ===La caméra : le point de vue depuis l'écran=== 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 les consoles de jeu 3DO, PS1, Sega Saturn. De nos jours, on utilise uniquement la technique de placage de texture inverse. Les deux seront décrites dans le détail plus bas. ===La différence entre rastérisation et lancer de rayons=== [[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 et les textures== 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. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Tampon de profondeur | Placage de textures |} Cependant, faire cela pose un problème : les objets transparents ne marchent pas ! Et pour comprendre pourquoi, ainsi que comment corriger ce problème, nous allons devoir expliquer la notion de fragment. ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Mettons qu'on regarde un objet semi-transparent, qui est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, on ne voit pas l'objet semi-transparent et seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Elle agit comme un coefficient qui dit comment mélanger la couleur de l'objet transparent et celui de l'objet opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Pour résumer, le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] ===Les fragments et les ROPs=== Maintenant, qu'en est-il pour ce qui est du rendu 3D ? Le mélange est réalisé pixel par pixel. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Il connait notamment une composante ''alpha'', qui est ajouté aux trois couleurs RGB. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Si les objets sont opaques et le fragment le plus proche est sélectionné. Mais avec des fragments transparents, les choses sont plus compliquées, car la couleur final est un mélange de plusieurs fragments non-cachés. Vu que plusieurs fragments sont censés être visibles, on ne sait pas quelle coordonnée z stocker. Il y a donc une interaction entre tampon de profondeur et mélange ''alpha''. Le tampon de profondeur est comme désactivé pour les fragments transparents, il n'est appliqué que sur les objets opaques. Pour appliquer le mélange ''alpha'' correctement, la profondeur des fragments est gérée en même temps que la transparence, par un même circuit. Il est appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. En effet, il faut connaitre la couleur final d'un fragment, pour faire les calculs. Et celle-ci n'est connue qu'en sortie de l'unité de texture, au plus tôt. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ==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.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <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. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|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. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[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).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[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. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, 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 par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. 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. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] 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. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] 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 différence entre l'éclairage par pixel et par sommet 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: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]] |} ===Les ''shaders'' : des programmes exécutés sur le GPU=== Maintenant que nous venons de voir les algorithmes d'éclairages, il est temps de voir comment les réaliser sur une carte graphique. Nous venons de voir qu'il y a une différence entre l'éclairage par pixel et par sommet. Intuitivement, l'éclairage par sommet devrait se faire avec les calculs géométriques, alors que l'éclairage par pixel devrait se faire après avoir appliqué les textures. Les toutes premières cartes graphiques ne géraient ni l'éclairage par sommet, ni l'éclairage par pixel. Elles laissaient les calculs géométriques au CPU. Par la suite, la Geforce 256 a intégré '''circuit de ''Transform & Lightning''''', qui s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). Elle gérait alors l'éclairage par sommet, mais un algorithme particulier, qui n'était pas très flexible. Il ne gérait que des ''material'' bien précis (des ''Phong materials''), rien de plus. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Unité de T&L : géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} L'amélioration suivante est venue sur la Geforce 3 : l'unité de T&L est devenue programmable. Au vu le grand nombre d'algorithmes d'éclairages possibles et le grand nombre de ''materials'' possibles, c'était la seule voie possibles. Les programmeurs pouvaient programmer leurs propres algorithmes d'éclairage par sommet, même s'ils devaient aussi programmer les étapes de transformation et de projection. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs. Ce qui est important est que la Geforce 3 a introduit une fonctionnalité absolument cruciale pour le rendu 3D moderne : les '''''shaders'''''. Il s'agit de programmes informatiques exécutés par la carte graphique, qui servaient initialement à coder des algorithmes d'éclairage. D'où leur nom : ''shader'' pour ''shading'' (éclairage en anglais). Cependant, l'usage modernes des shaders dépasse le cadre des algorithmes d'éclairage. L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Pas besoin de les intégrer dans la carte graphique pour les utiliser, pas besoin d'un circuit distinct pour chaque algorithme. Sans shaders, si la carte graphique ne gère pas un algorithme d'éclairage, on ne peut pas l'utiliser. A la rigueur, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique, avec des performances plus que convenables. Il existe plusieurs types de shaders, mais les deux principaux sont les '''''vertex shaders''''' et les '''''pixel shaders'''''. Les pixels shaders s'occupent de l'éclairage par pixel, leur nom est assez parlent. Les vertex shaders s'occupent de l'éclairage par sommet, mais aussi des étapes de transformation/projection. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux. Pour implémenter les shaders, il a fallu ajouter des processeurs à la carte graphique. Les processeurs en question exécutent les shaders, ils peuvent lire ou écrire dans des textures, mais ne font rien d'autres. Les ''vertex shaders'' font tout ce qui a trait à la géométrie, ils remplacent l'unité de T&L. Les pixels shaders sont entre la rastérisation et les ROPs, ils sont très liés à l'unité de texture. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Vertex shader'' | rowspan="2" | Rastérisation | Placage de textures | rowspan="2" |''Raster Operations Pipeline'' |- | class="f_rouge" | ''Pixel shader'' |} {{NavChapitre | book=Les cartes graphiques | prev=Les cartes d'affichage des anciens PC | prevText=Les cartes d'affichage des anciens PC | next=Les cartes graphiques : architecture de base | nextText=Les cartes graphiques : architecture de base }}{{autocat}} oe7s7rm3rndxvp9pss96esxpd7gq3g2 763495 763494 2026-04-11T18:08:59Z Mewtow 31375 /* La caméra : le point de vue depuis l'écran */ 763495 wikitext text/x-wiki Le premier jeu à utiliser de la "vraie 3D" texturée fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D. ==Les bases du rendu 3D== 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.]] Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Les sommets sont regroupés en triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie. ===La caméra : le point de vue depuis l'écran=== 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 les consoles de jeu 3DO, PS1, Sega Saturn. De nos jours, on utilise uniquement la technique de placage de texture inverse. Les deux seront décrites dans le détail plus bas. ===La différence entre rastérisation et lancer de rayons=== [[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 et les textures== 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. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Tampon de profondeur | Placage de textures |} Cependant, faire cela pose un problème : les objets transparents ne marchent pas ! Et pour comprendre pourquoi, ainsi que comment corriger ce problème, nous allons devoir expliquer la notion de fragment. ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Mettons qu'on regarde un objet semi-transparent, qui est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, on ne voit pas l'objet semi-transparent et seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Elle agit comme un coefficient qui dit comment mélanger la couleur de l'objet transparent et celui de l'objet opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Pour résumer, le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] ===Les fragments et les ROPs=== Maintenant, qu'en est-il pour ce qui est du rendu 3D ? Le mélange est réalisé pixel par pixel. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Il connait notamment une composante ''alpha'', qui est ajouté aux trois couleurs RGB. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Si les objets sont opaques et le fragment le plus proche est sélectionné. Mais avec des fragments transparents, les choses sont plus compliquées, car la couleur final est un mélange de plusieurs fragments non-cachés. Vu que plusieurs fragments sont censés être visibles, on ne sait pas quelle coordonnée z stocker. Il y a donc une interaction entre tampon de profondeur et mélange ''alpha''. Le tampon de profondeur est comme désactivé pour les fragments transparents, il n'est appliqué que sur les objets opaques. Pour appliquer le mélange ''alpha'' correctement, la profondeur des fragments est gérée en même temps que la transparence, par un même circuit. Il est appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. En effet, il faut connaitre la couleur final d'un fragment, pour faire les calculs. Et celle-ci n'est connue qu'en sortie de l'unité de texture, au plus tôt. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ==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.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <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. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|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. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[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).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[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. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, 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 par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. 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. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] 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. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] 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 différence entre l'éclairage par pixel et par sommet 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: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]] |} ===Les ''shaders'' : des programmes exécutés sur le GPU=== Maintenant que nous venons de voir les algorithmes d'éclairages, il est temps de voir comment les réaliser sur une carte graphique. Nous venons de voir qu'il y a une différence entre l'éclairage par pixel et par sommet. Intuitivement, l'éclairage par sommet devrait se faire avec les calculs géométriques, alors que l'éclairage par pixel devrait se faire après avoir appliqué les textures. Les toutes premières cartes graphiques ne géraient ni l'éclairage par sommet, ni l'éclairage par pixel. Elles laissaient les calculs géométriques au CPU. Par la suite, la Geforce 256 a intégré '''circuit de ''Transform & Lightning''''', qui s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). Elle gérait alors l'éclairage par sommet, mais un algorithme particulier, qui n'était pas très flexible. Il ne gérait que des ''material'' bien précis (des ''Phong materials''), rien de plus. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Unité de T&L : géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} L'amélioration suivante est venue sur la Geforce 3 : l'unité de T&L est devenue programmable. Au vu le grand nombre d'algorithmes d'éclairages possibles et le grand nombre de ''materials'' possibles, c'était la seule voie possibles. Les programmeurs pouvaient programmer leurs propres algorithmes d'éclairage par sommet, même s'ils devaient aussi programmer les étapes de transformation et de projection. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs. Ce qui est important est que la Geforce 3 a introduit une fonctionnalité absolument cruciale pour le rendu 3D moderne : les '''''shaders'''''. Il s'agit de programmes informatiques exécutés par la carte graphique, qui servaient initialement à coder des algorithmes d'éclairage. D'où leur nom : ''shader'' pour ''shading'' (éclairage en anglais). Cependant, l'usage modernes des shaders dépasse le cadre des algorithmes d'éclairage. L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Pas besoin de les intégrer dans la carte graphique pour les utiliser, pas besoin d'un circuit distinct pour chaque algorithme. Sans shaders, si la carte graphique ne gère pas un algorithme d'éclairage, on ne peut pas l'utiliser. A la rigueur, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique, avec des performances plus que convenables. Il existe plusieurs types de shaders, mais les deux principaux sont les '''''vertex shaders''''' et les '''''pixel shaders'''''. Les pixels shaders s'occupent de l'éclairage par pixel, leur nom est assez parlent. Les vertex shaders s'occupent de l'éclairage par sommet, mais aussi des étapes de transformation/projection. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux. Pour implémenter les shaders, il a fallu ajouter des processeurs à la carte graphique. Les processeurs en question exécutent les shaders, ils peuvent lire ou écrire dans des textures, mais ne font rien d'autres. Les ''vertex shaders'' font tout ce qui a trait à la géométrie, ils remplacent l'unité de T&L. Les pixels shaders sont entre la rastérisation et les ROPs, ils sont très liés à l'unité de texture. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Vertex shader'' | rowspan="2" | Rastérisation | Placage de textures | rowspan="2" |''Raster Operations Pipeline'' |- | class="f_rouge" | ''Pixel shader'' |} {{NavChapitre | book=Les cartes graphiques | prev=Les cartes d'affichage des anciens PC | prevText=Les cartes d'affichage des anciens PC | next=Les cartes graphiques : architecture de base | nextText=Les cartes graphiques : architecture de base }}{{autocat}} i4oc053f7lzjaw30tlfvox3sxhmh6zh Les cartes graphiques/Le pipeline géométrique d'un GPU 0 79241 763445 763430 2026-04-11T13:51:03Z Mewtow 31375 763445 wikitext text/x-wiki Dans le chapitre précédent, nous avons vu qu'il y a une différence entre le pipeline géométrique des anciennes stations de travail et des ordinateurs personnels. Les premiers tendaient à utiliser des processeurs flottants, programmés avec un ''firmware/microcode'' non-modifiable. Les ordinateurs personnels ont eu commencé avec des circuits géométriques fixe, pour les rendre de plus en plus programmables. Dans ce chapitre, nous allons étudier les circuits géométriques d'un GPU d'ordinateur personnel, et voir comment ils ont évolués dans le temps. ==L'''input assembler'' et l'assemblage de primitives== Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du cube de l'image ci-contre. Le sommet rouge du cube appartient aux 3 faces grise, jaune et bleue, et sera présent en trois exemplaires dans le tampon de sommets : un pour la face bleue, un pour la jaune, et un pour la grise. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : On pourrait remplacer les indices par des pointeurs, ce qui donnerait un cas particulier d'une structure de données connue sous le nom de vecteur de Liffe. Mais ce n'est pas très pratique et n'est pas utilisé dans le domaine du rendu 3D. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L'''input assembler'' et le tampon d'indice=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Pour faire son travail, il a besoin de l'adresse des données géométriques en mémoire, leur taille et éventuellement du type des données qu'on lui envoie (sommets codées sur 32 bits, 64, 128, etc). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. Et en général, c'est une unité d'accès mémoire un peu particulière, qui contient des circuits assez classiques pour ce genre de circuits : des circuits de calcul d'adresse, des circuits pour commander la mémoire VRAM, un contrôleur mémoire, diverses mémoires tampons, etc. Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques. [[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]] Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela. Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice. Les unités de ''index fetch'' et de ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances. Vous remarquerez que l’''input assembler'' fait surtout des calculs d'adresse, des lectures en mémoire, et des conversions de format de données. Un processeur de ''vertex shader'' peut faire la même chose, ce qui fait qu'il est possible d'émuler l'''input assembler'' avec un ''vertex shader''. La seule condition, absolument nécessaire, est que le ''vertex shader'' puisse lire des données en mémoire vidéo. Et pas seulement lire des textures, comme le permettent les techniques de ''vertex texturing'', mais de vraies lectures arbitraires, pour lire les tampons de sommet/indice. Cette possibilité est arrivée avec Direct X 10, ce qui fait que l’''input assembler'' peut être émulé par les ''vertex shaders'' à partir de cette version de Direct X. De nos jours, tous les GPUs font à leur sauce. Certains émulent l’''input assembler'' avec des ''shaders'', d'autres non. Ceux qui le font le font en modifiant les ''vertex shaders''. Le ''driver'' du GPU injecte du code dans les ''vertex shaders'', code qui émule l'''input assembler''. ===Les caches de sommets : une optimisation du tampon d'indice=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais quand des sommets sont dupliqués, ce n'est pas le cas. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Elle est appelée le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants. L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''. Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''. Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide. Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite. [[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]] Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances. [[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]] Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'assemblage de primitives=== En sortie des unités géométriques, on a des sommets éclairés et colorisés, pas des triangles. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. Un problème pour l'assemblage de primitives est que les sommets n’arrivent pas dans l'ordre. Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées. Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle. Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles. Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. ==L'étape de T&L et les vertex shaders== L'''input assembler'' est suivi par une étape de '''transformation-projection'''. Elle regroupe plusieurs manipulations différentes, mais qui 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 3D du point de vue de la caméra, et un autre qui corrige la perspective. Un changement de coordonnée s'effectue assez 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 petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes, et que 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 de détail. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée unité de T&L (Transform & Lighting). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'input assembler, ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. L'unité de T&L est devenue programmable dès la Geforce 3, première carte graphique à supporter les vertex shaders. Il y a peu de généralités spécifiques pour les processeurs de vertex shaders, tout a déjà été dit dans le chapitre sur les processeurs de shaders. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des vertex shaders, distincts des processeurs pour les pixel shaders. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de vertex shader d’antan. ==DirectX 10 : les ''geometry shaders''== Les GPU d'avant DirectX 10, qui n'avaient que les ''vertex shaders'' et ne pouvaient manipuler que des sommets. Depuis DirectX 10, le pipeline graphique a intégré des techniques pour gérer nativement des triangles dans les ''shaders''. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. L'intérêt est que cela permet de faciliter l'implémentation de techniques de tesselation, sans compter que certaines optimisations deviennent plus simples à effectuer. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. DirectX 10 et OpenGl 3.2 ont introduit les ''geometry shaders'', juste avant l'étape d'assemblage des primitives. Les ''geometry shaders'' peuvent ajouter, supprimer ou altérer des primitives dans une scène 3D. Un ''geometry shader'' prend en entrée un point, une ligne ou un triangle, donc les trois primitives de base supportées sur les GPU modernes. Il émet en sortie : soit un ''triangle strip'', soit une ''line strip'' (c'est à une ligne ce qu'un d'un ''triangle strip'' est à un triangle) ou un point. Ils n'ont pas été très utilisés, leurs utilisations étant assez limitées. Ils peuvent en théorie être utilisés pour la gestion des ''cubemaps'', le ''shadow volume extrusion'', la génération de particules, et quelques autres effets graphiques. Ils pourraient aussi être utilisés pour faire de la tesselation, mais leurs limitations font que ce n'est pas pratique. Rappelons que les ''geometry shaders'' sont optionnels et que beaucoup de jeux vidéos ou de moteurs de rendu 3D n'en utilisent pas. ===L'étape d’assemblage de primitives est dupliquée=== Les ''geometry shaders'' n'ont jamais eu de processeur de shader dédié, car ils ont été introduits avec DirectX 10 et OpenGl 3.2, en même temps que les processeurs de ''shaders'' ont étés unifiés (rendu capable d’exécuter n'importe quel ''shader''). Leur place dans le pipeline graphique est quelque peu étrange. En théorie, ils sont placés après l'assembleur de primitive, car ils manipulent les primitives fournies par l'étape d'assemblage des primitives. Mais le résultat fournit par les ''geometry shaders'' doit être retraité par l'assembleur de primitive. En effet, j'ai menti plus haut en disant que les ''geometry shaders'' fournissent en entrée de 0 à plusieurs primitives : la sortie d'un ''geometry shader'' est un ensemble de sommets, non-regroupés en primitives. Le résultat est que l'assembleur de primitive doit refaire son travail après le passage d'un ''geometry shader'', pour déterminer les primitives finales. Et il faut aussi refaire le ''culling'', au cas où les primitives générées ne soient pas visibles depuis la caméra. Heureusement, la sortie d'un ''geometry shader'' est soit un point, soit une ligne, soit un ''triangle strip'', ce qui simplifie la seconde phase d'assemblage des primitives. Avec les ''geometry shaders'', il y a donc deux phases d'assemblage des primitives : une phase avant, décrite dans la section précédente, et une seconde phase simplifiée après les ''geometry shaders''. Il n'y a pas que la phase d'assemblage de primitives qui est dupliquée : le tampon de primitives l'est aussi. On trouve donc un tampon de primitives à l'entrée des ''geometry shaders'' et un autre à la sortie. [[File:Implémentation matérielle des geometry shaders.png|centre|vignette|upright=2|Implémentation matérielle des geometry shaders]] L'implémentation des tampons de primitive est assez compliquée par la spécification des ''geometry shaders''. Un ''geometry shader'' fournit un résultat très variable en fonction de ses entrées. Pour une même entrée, la sortie peut aller d'une simple primitive à plusieurs dizaines. Le ''geometry shader'' précise cependant un nombre limite de sommets qu'il ne peut pas dépasser en sortie. Il peut ainsi préciser qu'il ne sortira pas plus de 16 sommets, par exemple. Mais ce nombre est généralement très élevé, bien plus que la moyenne réelle du résultat du ''geometry shader''. Or, le tampon de primitives de sortie a une taille finie qui doit être partagée entre plusieurs instances du ''geometry shader''. Et cette répartition n'est pas dynamique, mais statique : chaque instance reçoit une certaine portion du tampon de primitive, égale à la taille du tampon de primitives divisée par ce nombre limite. Aussi, le nombre d'instance exécutables en parallèle est rapidement limitée par le nombre de sommets maximal que peut sortir le ''geometry shader'', nombre qui est rarement atteint en pratique. ===La fonctionnalité de ''stream output''=== Une fonctionnalité des ''geometry shaders'' est la possibilité d'enregistrer leurs résultats en mémoire. Il s'agit de la fonctionnalité du '''''stream output'''''. On peut ainsi remplir une texture ou le ''vertex buffer'' dans la mémoire vidéo, avec le résultat d'un ''geometry shader''. Notons que celle-ci mémorise un ensemble de primitives, pas autre chose. Cette fonctionnalité est utilisée pour certains effets ou rendu bien précis, mais il faut avouer qu'elle n'est pas très souvent utilisée. Aussi, les concepteurs de cartes graphiques n'ont pas optimisé cette fonctionnalité au maximum. Le ''stream output'' n'a généralement pas accès prioritaire à la mémoire, comparé aux ROP, et n'a souvent accès qu'à une partie limitée de la bande passante mémoire. Notons qu'il existe deux formes de ''stream output'' : une qui permet aux ''vertex shader'' d'écrire dans une texture, l'autre qui permet aux ''geometry shaders'' de le faire. Notons que le ''stream output'' fournit un flux de primitives, pas de sommets, même pour le flux sortant d'un ''vertex shader''. En clair, beaucoup de sommets sont dupliqués et ont n'a pas d{{'}}''index buffer''. Les résultats du ''stream output'' sont donc assez lourds et prennent beaucoup de mémoire. [[File:Stream output.png|centre|vignette|upright=2.5|Stream output]] ==DirectX 12 : les ''mesh shaders''== [[File:D3D11 Pipeline.svg|vignette|upright=1|Pipeline graphique de Direct x 11.]] Avec l'introduction des ''geometry shaders'' et de la tesselation, le pipeline graphique est devenu très complexe. Plusieurs étages en plus sont ajoutés à sa portion géométrique : un pour les ''geometry shaders'', trois pour la tesselation, et ce en plus des ''vertex shaders'' existants et des étages non-programmables. Le pipeline en question est celui d'Open GL 4 et de DirectX 11. Mais Direct X 12 a simplifié le tout, sous l'impulsion de technologies introduites par AMD et de NVIDIA. AMD a introduit les ''primitive shaders'', NVIDIA a introduit les ''mesh shaders'''' ont été introduit par NVIDIA. Les derniers ont été gardés pour DirectX 12, simplifiant grandement le pipeline. ===Les primitive/mesh shaders=== Les deux solutions de AMD et NVIDIA partent du même principe : elles fusionnent certaines étapes du pipeline. Les ''primitive/mesh shaders'' font disparaitre les étapes d{{'}}''input assembly'' et d'assemblage de primitives, qui sont maintenant gérées par les ''primitive/mesh shaders''. Les ''primitive/mesh shaders'' lisent directement le tampon d'indice et lisent les sommets depuis la VRAM, sans passer par une étape non-programmable. Ils assemblent les primitives eux-mêmes et les envoient directement au rastériseur. Le tout permet des optimisations très intéressantes, comme un ''culling'' précoce. Les ''mesh shaders'' sont des ''shaders'' généralistes, semblables aux ''compute shaders''. Pour rappel, un ''compute shader'' peut lire des données en RAM, exécuter des traitements dessus, et enregistrer les résultats en RAM. Il peut lire ou écrire à des adresses arbitraires, sans limitations. Il n'est pas limité à lire des données consécutives, peut sauter d'une donnée à une autre donnée distante en RAM. Les ''mesh shaders'' sont des variantes des ''compute shaders'', qui n'écrivent pas leur résultat en RAM, mais envoient celui-ci au rastériseur. Plus précisément, ils écrivent leur résultat dans le tampon de primitives. Les ''mesh shaders'' peuvent contourner l'étape d{{'}}''input assembly'' et la remplacer par leur propre code. Pour rappel, l'étape d{{'}}''input assembly'' était non-programmable et gérait des tampons de vertices et d'indices très normés. Les sommets étaient lus soit un par un, soit par paquets de N sommets consécutifs, ce qui était assez rigide. Il n'y avait pas d'accès arbitraire en mémoire RAM comme peuvent le faire les ''compute shaders''. Par contre, un ''mesh shader'' peut accéder aux sommets de la manière qu'il souhaite, ce qui permet d'émuler un ''input assembler'' normal et plus encore. Une autre différence avec les ''vertex shaders'' est qu'ils ne traitent pas forcément des sommets, mais peuvent aussi envoyer des primitives au rastériseur directement. En clair, ils n'ont pas besoin d'une étape de ''primitive assembly'', qu'ils peuvent émuler directement dans le ''shader'' lui-même. Le ''culling'' est lui aussi réalisé par le ''primitive shader'', pas par une unité fixe. Et cela permet de contourner un problème fondamental des ''vertex shaders'' : il fallait que les primitives soient assemblées pour qu'on puisse déterminer si elles sont ou non invisibles. A l'opposé, les ''primitive/mesh shaders'' assemblent les primitives de manière précoce dans le ''primitive/mesh shader'', ce qui permet d'éliminer les primitives invisibles le plus tôt possible. Pour cela, les opérations permettant de déterminer si une primitive est visible sont exécutés en priorité, les autres opérations sont retardées et effectuées le plus tard possible. Ainsi, les calculs pour colorier ou orienter un sommet ne sont pas exécutés si le sommet est invisible. Il y a des différences entre ''primitive'' et ''mesh shaders''. Les ''primitive shaders'' permettent de lire un sommet à la fois, alors que les ''mesh shaders'' permettent de lire des ''batchs'' de plusieurs primitives d'un coup. Ces ''batchs'' de plusieurs primitives sont appelés des meshlets. La différence n'est pas fondamentale : le hardware des cartes AMD, qui gère des ''primitive shaders'', peut regrouper dynamiquement plusieurs instances de ''primitive shaders'' en un seul ''mesh shader'', via les technique de SIMT (une instance de ''primitive shader'' effectue des opérations scalaires, qui peuvent être regroupées en une seule instance SIMD en traitant plusieurs sommets en parallèle). La seule différence est que les ''mesh shaders'' exposent ce comportement au niveau du jeu d'instruction des ''shaders'', les programmeurs en ont conscience. ===Le pipeline géométrique avec les ''primitive/mesh shaders''=== Avec les ''primitive shaders'', l'implémentation exacte dépend de si la tesselation est activée ou non. Si la tesselation n'est pas activée, le ''vertex shader'' et le ''geométry shader'' sont fusionnés en un seul ''primitive shader''. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, sans tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="4" | |- ! DirectX 12 | colspan="4" | ''Primitive shader'' (AMD) |} Avec la tesselation activée, les ''geometry shaders'' et les ''domain shaders'' en un seul ''shader''. De même, les ''vertex shaders'' et les ''hull shaders'' sont fusionnés en un seul ''shader'', nommé l{{'}}''amplification shader''. Ainsi, le pipeline graphique est grandement simplifié, avec seulement deux ''shaders'' et un étage fixe, au lieu de quatre ''shaders'' différents. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, avec tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="7" | |- ! DirectX 12 | colspan="3" | * ''Amplification shader'' (AMD) | class="f_rouge" | Tesselation | colspan="3" | * ''Primitive shader'' (AMD) |} <noinclude> {{NavChapitre | book=Les cartes graphiques | prev=Le pipeline géométrique d'avant DirectX 10 | prevText=Le pipeline géométrique d'avant DirectX 10 | next=Le rasterizeur | nextText=Le rasterizeur }}{{autocat}} </noinclude> tnb22t1vjcffhtcev29b6p5ap06zzlx 763461 763445 2026-04-11T16:01:03Z Mewtow 31375 Mewtow a déplacé la page [[Les cartes graphiques/Le pipeline géométrique après DirectX 10]] vers [[Les cartes graphiques/Le pipeline géométrique d'un GPU]] 763445 wikitext text/x-wiki Dans le chapitre précédent, nous avons vu qu'il y a une différence entre le pipeline géométrique des anciennes stations de travail et des ordinateurs personnels. Les premiers tendaient à utiliser des processeurs flottants, programmés avec un ''firmware/microcode'' non-modifiable. Les ordinateurs personnels ont eu commencé avec des circuits géométriques fixe, pour les rendre de plus en plus programmables. Dans ce chapitre, nous allons étudier les circuits géométriques d'un GPU d'ordinateur personnel, et voir comment ils ont évolués dans le temps. ==L'''input assembler'' et l'assemblage de primitives== Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du cube de l'image ci-contre. Le sommet rouge du cube appartient aux 3 faces grise, jaune et bleue, et sera présent en trois exemplaires dans le tampon de sommets : un pour la face bleue, un pour la jaune, et un pour la grise. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : On pourrait remplacer les indices par des pointeurs, ce qui donnerait un cas particulier d'une structure de données connue sous le nom de vecteur de Liffe. Mais ce n'est pas très pratique et n'est pas utilisé dans le domaine du rendu 3D. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L'''input assembler'' et le tampon d'indice=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Pour faire son travail, il a besoin de l'adresse des données géométriques en mémoire, leur taille et éventuellement du type des données qu'on lui envoie (sommets codées sur 32 bits, 64, 128, etc). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. Et en général, c'est une unité d'accès mémoire un peu particulière, qui contient des circuits assez classiques pour ce genre de circuits : des circuits de calcul d'adresse, des circuits pour commander la mémoire VRAM, un contrôleur mémoire, diverses mémoires tampons, etc. Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques. [[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]] Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela. Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice. Les unités de ''index fetch'' et de ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances. Vous remarquerez que l’''input assembler'' fait surtout des calculs d'adresse, des lectures en mémoire, et des conversions de format de données. Un processeur de ''vertex shader'' peut faire la même chose, ce qui fait qu'il est possible d'émuler l'''input assembler'' avec un ''vertex shader''. La seule condition, absolument nécessaire, est que le ''vertex shader'' puisse lire des données en mémoire vidéo. Et pas seulement lire des textures, comme le permettent les techniques de ''vertex texturing'', mais de vraies lectures arbitraires, pour lire les tampons de sommet/indice. Cette possibilité est arrivée avec Direct X 10, ce qui fait que l’''input assembler'' peut être émulé par les ''vertex shaders'' à partir de cette version de Direct X. De nos jours, tous les GPUs font à leur sauce. Certains émulent l’''input assembler'' avec des ''shaders'', d'autres non. Ceux qui le font le font en modifiant les ''vertex shaders''. Le ''driver'' du GPU injecte du code dans les ''vertex shaders'', code qui émule l'''input assembler''. ===Les caches de sommets : une optimisation du tampon d'indice=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais quand des sommets sont dupliqués, ce n'est pas le cas. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Elle est appelée le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants. L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''. Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''. Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide. Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite. [[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]] Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances. [[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]] Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'assemblage de primitives=== En sortie des unités géométriques, on a des sommets éclairés et colorisés, pas des triangles. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. Un problème pour l'assemblage de primitives est que les sommets n’arrivent pas dans l'ordre. Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées. Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle. Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles. Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. ==L'étape de T&L et les vertex shaders== L'''input assembler'' est suivi par une étape de '''transformation-projection'''. Elle regroupe plusieurs manipulations différentes, mais qui 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 3D du point de vue de la caméra, et un autre qui corrige la perspective. Un changement de coordonnée s'effectue assez 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 petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes, et que 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 de détail. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée unité de T&L (Transform & Lighting). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'input assembler, ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. L'unité de T&L est devenue programmable dès la Geforce 3, première carte graphique à supporter les vertex shaders. Il y a peu de généralités spécifiques pour les processeurs de vertex shaders, tout a déjà été dit dans le chapitre sur les processeurs de shaders. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des vertex shaders, distincts des processeurs pour les pixel shaders. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de vertex shader d’antan. ==DirectX 10 : les ''geometry shaders''== Les GPU d'avant DirectX 10, qui n'avaient que les ''vertex shaders'' et ne pouvaient manipuler que des sommets. Depuis DirectX 10, le pipeline graphique a intégré des techniques pour gérer nativement des triangles dans les ''shaders''. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. L'intérêt est que cela permet de faciliter l'implémentation de techniques de tesselation, sans compter que certaines optimisations deviennent plus simples à effectuer. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. DirectX 10 et OpenGl 3.2 ont introduit les ''geometry shaders'', juste avant l'étape d'assemblage des primitives. Les ''geometry shaders'' peuvent ajouter, supprimer ou altérer des primitives dans une scène 3D. Un ''geometry shader'' prend en entrée un point, une ligne ou un triangle, donc les trois primitives de base supportées sur les GPU modernes. Il émet en sortie : soit un ''triangle strip'', soit une ''line strip'' (c'est à une ligne ce qu'un d'un ''triangle strip'' est à un triangle) ou un point. Ils n'ont pas été très utilisés, leurs utilisations étant assez limitées. Ils peuvent en théorie être utilisés pour la gestion des ''cubemaps'', le ''shadow volume extrusion'', la génération de particules, et quelques autres effets graphiques. Ils pourraient aussi être utilisés pour faire de la tesselation, mais leurs limitations font que ce n'est pas pratique. Rappelons que les ''geometry shaders'' sont optionnels et que beaucoup de jeux vidéos ou de moteurs de rendu 3D n'en utilisent pas. ===L'étape d’assemblage de primitives est dupliquée=== Les ''geometry shaders'' n'ont jamais eu de processeur de shader dédié, car ils ont été introduits avec DirectX 10 et OpenGl 3.2, en même temps que les processeurs de ''shaders'' ont étés unifiés (rendu capable d’exécuter n'importe quel ''shader''). Leur place dans le pipeline graphique est quelque peu étrange. En théorie, ils sont placés après l'assembleur de primitive, car ils manipulent les primitives fournies par l'étape d'assemblage des primitives. Mais le résultat fournit par les ''geometry shaders'' doit être retraité par l'assembleur de primitive. En effet, j'ai menti plus haut en disant que les ''geometry shaders'' fournissent en entrée de 0 à plusieurs primitives : la sortie d'un ''geometry shader'' est un ensemble de sommets, non-regroupés en primitives. Le résultat est que l'assembleur de primitive doit refaire son travail après le passage d'un ''geometry shader'', pour déterminer les primitives finales. Et il faut aussi refaire le ''culling'', au cas où les primitives générées ne soient pas visibles depuis la caméra. Heureusement, la sortie d'un ''geometry shader'' est soit un point, soit une ligne, soit un ''triangle strip'', ce qui simplifie la seconde phase d'assemblage des primitives. Avec les ''geometry shaders'', il y a donc deux phases d'assemblage des primitives : une phase avant, décrite dans la section précédente, et une seconde phase simplifiée après les ''geometry shaders''. Il n'y a pas que la phase d'assemblage de primitives qui est dupliquée : le tampon de primitives l'est aussi. On trouve donc un tampon de primitives à l'entrée des ''geometry shaders'' et un autre à la sortie. [[File:Implémentation matérielle des geometry shaders.png|centre|vignette|upright=2|Implémentation matérielle des geometry shaders]] L'implémentation des tampons de primitive est assez compliquée par la spécification des ''geometry shaders''. Un ''geometry shader'' fournit un résultat très variable en fonction de ses entrées. Pour une même entrée, la sortie peut aller d'une simple primitive à plusieurs dizaines. Le ''geometry shader'' précise cependant un nombre limite de sommets qu'il ne peut pas dépasser en sortie. Il peut ainsi préciser qu'il ne sortira pas plus de 16 sommets, par exemple. Mais ce nombre est généralement très élevé, bien plus que la moyenne réelle du résultat du ''geometry shader''. Or, le tampon de primitives de sortie a une taille finie qui doit être partagée entre plusieurs instances du ''geometry shader''. Et cette répartition n'est pas dynamique, mais statique : chaque instance reçoit une certaine portion du tampon de primitive, égale à la taille du tampon de primitives divisée par ce nombre limite. Aussi, le nombre d'instance exécutables en parallèle est rapidement limitée par le nombre de sommets maximal que peut sortir le ''geometry shader'', nombre qui est rarement atteint en pratique. ===La fonctionnalité de ''stream output''=== Une fonctionnalité des ''geometry shaders'' est la possibilité d'enregistrer leurs résultats en mémoire. Il s'agit de la fonctionnalité du '''''stream output'''''. On peut ainsi remplir une texture ou le ''vertex buffer'' dans la mémoire vidéo, avec le résultat d'un ''geometry shader''. Notons que celle-ci mémorise un ensemble de primitives, pas autre chose. Cette fonctionnalité est utilisée pour certains effets ou rendu bien précis, mais il faut avouer qu'elle n'est pas très souvent utilisée. Aussi, les concepteurs de cartes graphiques n'ont pas optimisé cette fonctionnalité au maximum. Le ''stream output'' n'a généralement pas accès prioritaire à la mémoire, comparé aux ROP, et n'a souvent accès qu'à une partie limitée de la bande passante mémoire. Notons qu'il existe deux formes de ''stream output'' : une qui permet aux ''vertex shader'' d'écrire dans une texture, l'autre qui permet aux ''geometry shaders'' de le faire. Notons que le ''stream output'' fournit un flux de primitives, pas de sommets, même pour le flux sortant d'un ''vertex shader''. En clair, beaucoup de sommets sont dupliqués et ont n'a pas d{{'}}''index buffer''. Les résultats du ''stream output'' sont donc assez lourds et prennent beaucoup de mémoire. [[File:Stream output.png|centre|vignette|upright=2.5|Stream output]] ==DirectX 12 : les ''mesh shaders''== [[File:D3D11 Pipeline.svg|vignette|upright=1|Pipeline graphique de Direct x 11.]] Avec l'introduction des ''geometry shaders'' et de la tesselation, le pipeline graphique est devenu très complexe. Plusieurs étages en plus sont ajoutés à sa portion géométrique : un pour les ''geometry shaders'', trois pour la tesselation, et ce en plus des ''vertex shaders'' existants et des étages non-programmables. Le pipeline en question est celui d'Open GL 4 et de DirectX 11. Mais Direct X 12 a simplifié le tout, sous l'impulsion de technologies introduites par AMD et de NVIDIA. AMD a introduit les ''primitive shaders'', NVIDIA a introduit les ''mesh shaders'''' ont été introduit par NVIDIA. Les derniers ont été gardés pour DirectX 12, simplifiant grandement le pipeline. ===Les primitive/mesh shaders=== Les deux solutions de AMD et NVIDIA partent du même principe : elles fusionnent certaines étapes du pipeline. Les ''primitive/mesh shaders'' font disparaitre les étapes d{{'}}''input assembly'' et d'assemblage de primitives, qui sont maintenant gérées par les ''primitive/mesh shaders''. Les ''primitive/mesh shaders'' lisent directement le tampon d'indice et lisent les sommets depuis la VRAM, sans passer par une étape non-programmable. Ils assemblent les primitives eux-mêmes et les envoient directement au rastériseur. Le tout permet des optimisations très intéressantes, comme un ''culling'' précoce. Les ''mesh shaders'' sont des ''shaders'' généralistes, semblables aux ''compute shaders''. Pour rappel, un ''compute shader'' peut lire des données en RAM, exécuter des traitements dessus, et enregistrer les résultats en RAM. Il peut lire ou écrire à des adresses arbitraires, sans limitations. Il n'est pas limité à lire des données consécutives, peut sauter d'une donnée à une autre donnée distante en RAM. Les ''mesh shaders'' sont des variantes des ''compute shaders'', qui n'écrivent pas leur résultat en RAM, mais envoient celui-ci au rastériseur. Plus précisément, ils écrivent leur résultat dans le tampon de primitives. Les ''mesh shaders'' peuvent contourner l'étape d{{'}}''input assembly'' et la remplacer par leur propre code. Pour rappel, l'étape d{{'}}''input assembly'' était non-programmable et gérait des tampons de vertices et d'indices très normés. Les sommets étaient lus soit un par un, soit par paquets de N sommets consécutifs, ce qui était assez rigide. Il n'y avait pas d'accès arbitraire en mémoire RAM comme peuvent le faire les ''compute shaders''. Par contre, un ''mesh shader'' peut accéder aux sommets de la manière qu'il souhaite, ce qui permet d'émuler un ''input assembler'' normal et plus encore. Une autre différence avec les ''vertex shaders'' est qu'ils ne traitent pas forcément des sommets, mais peuvent aussi envoyer des primitives au rastériseur directement. En clair, ils n'ont pas besoin d'une étape de ''primitive assembly'', qu'ils peuvent émuler directement dans le ''shader'' lui-même. Le ''culling'' est lui aussi réalisé par le ''primitive shader'', pas par une unité fixe. Et cela permet de contourner un problème fondamental des ''vertex shaders'' : il fallait que les primitives soient assemblées pour qu'on puisse déterminer si elles sont ou non invisibles. A l'opposé, les ''primitive/mesh shaders'' assemblent les primitives de manière précoce dans le ''primitive/mesh shader'', ce qui permet d'éliminer les primitives invisibles le plus tôt possible. Pour cela, les opérations permettant de déterminer si une primitive est visible sont exécutés en priorité, les autres opérations sont retardées et effectuées le plus tard possible. Ainsi, les calculs pour colorier ou orienter un sommet ne sont pas exécutés si le sommet est invisible. Il y a des différences entre ''primitive'' et ''mesh shaders''. Les ''primitive shaders'' permettent de lire un sommet à la fois, alors que les ''mesh shaders'' permettent de lire des ''batchs'' de plusieurs primitives d'un coup. Ces ''batchs'' de plusieurs primitives sont appelés des meshlets. La différence n'est pas fondamentale : le hardware des cartes AMD, qui gère des ''primitive shaders'', peut regrouper dynamiquement plusieurs instances de ''primitive shaders'' en un seul ''mesh shader'', via les technique de SIMT (une instance de ''primitive shader'' effectue des opérations scalaires, qui peuvent être regroupées en une seule instance SIMD en traitant plusieurs sommets en parallèle). La seule différence est que les ''mesh shaders'' exposent ce comportement au niveau du jeu d'instruction des ''shaders'', les programmeurs en ont conscience. ===Le pipeline géométrique avec les ''primitive/mesh shaders''=== Avec les ''primitive shaders'', l'implémentation exacte dépend de si la tesselation est activée ou non. Si la tesselation n'est pas activée, le ''vertex shader'' et le ''geométry shader'' sont fusionnés en un seul ''primitive shader''. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, sans tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="4" | |- ! DirectX 12 | colspan="4" | ''Primitive shader'' (AMD) |} Avec la tesselation activée, les ''geometry shaders'' et les ''domain shaders'' en un seul ''shader''. De même, les ''vertex shaders'' et les ''hull shaders'' sont fusionnés en un seul ''shader'', nommé l{{'}}''amplification shader''. Ainsi, le pipeline graphique est grandement simplifié, avec seulement deux ''shaders'' et un étage fixe, au lieu de quatre ''shaders'' différents. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, avec tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="7" | |- ! DirectX 12 | colspan="3" | * ''Amplification shader'' (AMD) | class="f_rouge" | Tesselation | colspan="3" | * ''Primitive shader'' (AMD) |} <noinclude> {{NavChapitre | book=Les cartes graphiques | prev=Le pipeline géométrique d'avant DirectX 10 | prevText=Le pipeline géométrique d'avant DirectX 10 | next=Le rasterizeur | nextText=Le rasterizeur }}{{autocat}} </noinclude> tnb22t1vjcffhtcev29b6p5ap06zzlx 763466 763461 2026-04-11T16:02:01Z Mewtow 31375 /* Le pipeline géométrique avec les primitive/mesh shaders */ 763466 wikitext text/x-wiki Dans le chapitre précédent, nous avons vu qu'il y a une différence entre le pipeline géométrique des anciennes stations de travail et des ordinateurs personnels. Les premiers tendaient à utiliser des processeurs flottants, programmés avec un ''firmware/microcode'' non-modifiable. Les ordinateurs personnels ont eu commencé avec des circuits géométriques fixe, pour les rendre de plus en plus programmables. Dans ce chapitre, nous allons étudier les circuits géométriques d'un GPU d'ordinateur personnel, et voir comment ils ont évolués dans le temps. ==L'''input assembler'' et l'assemblage de primitives== Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du cube de l'image ci-contre. Le sommet rouge du cube appartient aux 3 faces grise, jaune et bleue, et sera présent en trois exemplaires dans le tampon de sommets : un pour la face bleue, un pour la jaune, et un pour la grise. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : On pourrait remplacer les indices par des pointeurs, ce qui donnerait un cas particulier d'une structure de données connue sous le nom de vecteur de Liffe. Mais ce n'est pas très pratique et n'est pas utilisé dans le domaine du rendu 3D. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L'''input assembler'' et le tampon d'indice=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Pour faire son travail, il a besoin de l'adresse des données géométriques en mémoire, leur taille et éventuellement du type des données qu'on lui envoie (sommets codées sur 32 bits, 64, 128, etc). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. Et en général, c'est une unité d'accès mémoire un peu particulière, qui contient des circuits assez classiques pour ce genre de circuits : des circuits de calcul d'adresse, des circuits pour commander la mémoire VRAM, un contrôleur mémoire, diverses mémoires tampons, etc. Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques. [[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]] Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela. Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice. Les unités de ''index fetch'' et de ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances. Vous remarquerez que l’''input assembler'' fait surtout des calculs d'adresse, des lectures en mémoire, et des conversions de format de données. Un processeur de ''vertex shader'' peut faire la même chose, ce qui fait qu'il est possible d'émuler l'''input assembler'' avec un ''vertex shader''. La seule condition, absolument nécessaire, est que le ''vertex shader'' puisse lire des données en mémoire vidéo. Et pas seulement lire des textures, comme le permettent les techniques de ''vertex texturing'', mais de vraies lectures arbitraires, pour lire les tampons de sommet/indice. Cette possibilité est arrivée avec Direct X 10, ce qui fait que l’''input assembler'' peut être émulé par les ''vertex shaders'' à partir de cette version de Direct X. De nos jours, tous les GPUs font à leur sauce. Certains émulent l’''input assembler'' avec des ''shaders'', d'autres non. Ceux qui le font le font en modifiant les ''vertex shaders''. Le ''driver'' du GPU injecte du code dans les ''vertex shaders'', code qui émule l'''input assembler''. ===Les caches de sommets : une optimisation du tampon d'indice=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais quand des sommets sont dupliqués, ce n'est pas le cas. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Elle est appelée le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants. L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''. Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''. Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide. Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite. [[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]] Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances. [[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]] Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'assemblage de primitives=== En sortie des unités géométriques, on a des sommets éclairés et colorisés, pas des triangles. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. Un problème pour l'assemblage de primitives est que les sommets n’arrivent pas dans l'ordre. Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées. Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle. Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles. Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. ==L'étape de T&L et les vertex shaders== L'''input assembler'' est suivi par une étape de '''transformation-projection'''. Elle regroupe plusieurs manipulations différentes, mais qui 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 3D du point de vue de la caméra, et un autre qui corrige la perspective. Un changement de coordonnée s'effectue assez 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 petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes, et que 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 de détail. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée unité de T&L (Transform & Lighting). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'input assembler, ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. L'unité de T&L est devenue programmable dès la Geforce 3, première carte graphique à supporter les vertex shaders. Il y a peu de généralités spécifiques pour les processeurs de vertex shaders, tout a déjà été dit dans le chapitre sur les processeurs de shaders. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des vertex shaders, distincts des processeurs pour les pixel shaders. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de vertex shader d’antan. ==DirectX 10 : les ''geometry shaders''== Les GPU d'avant DirectX 10, qui n'avaient que les ''vertex shaders'' et ne pouvaient manipuler que des sommets. Depuis DirectX 10, le pipeline graphique a intégré des techniques pour gérer nativement des triangles dans les ''shaders''. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. L'intérêt est que cela permet de faciliter l'implémentation de techniques de tesselation, sans compter que certaines optimisations deviennent plus simples à effectuer. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. DirectX 10 et OpenGl 3.2 ont introduit les ''geometry shaders'', juste avant l'étape d'assemblage des primitives. Les ''geometry shaders'' peuvent ajouter, supprimer ou altérer des primitives dans une scène 3D. Un ''geometry shader'' prend en entrée un point, une ligne ou un triangle, donc les trois primitives de base supportées sur les GPU modernes. Il émet en sortie : soit un ''triangle strip'', soit une ''line strip'' (c'est à une ligne ce qu'un d'un ''triangle strip'' est à un triangle) ou un point. Ils n'ont pas été très utilisés, leurs utilisations étant assez limitées. Ils peuvent en théorie être utilisés pour la gestion des ''cubemaps'', le ''shadow volume extrusion'', la génération de particules, et quelques autres effets graphiques. Ils pourraient aussi être utilisés pour faire de la tesselation, mais leurs limitations font que ce n'est pas pratique. Rappelons que les ''geometry shaders'' sont optionnels et que beaucoup de jeux vidéos ou de moteurs de rendu 3D n'en utilisent pas. ===L'étape d’assemblage de primitives est dupliquée=== Les ''geometry shaders'' n'ont jamais eu de processeur de shader dédié, car ils ont été introduits avec DirectX 10 et OpenGl 3.2, en même temps que les processeurs de ''shaders'' ont étés unifiés (rendu capable d’exécuter n'importe quel ''shader''). Leur place dans le pipeline graphique est quelque peu étrange. En théorie, ils sont placés après l'assembleur de primitive, car ils manipulent les primitives fournies par l'étape d'assemblage des primitives. Mais le résultat fournit par les ''geometry shaders'' doit être retraité par l'assembleur de primitive. En effet, j'ai menti plus haut en disant que les ''geometry shaders'' fournissent en entrée de 0 à plusieurs primitives : la sortie d'un ''geometry shader'' est un ensemble de sommets, non-regroupés en primitives. Le résultat est que l'assembleur de primitive doit refaire son travail après le passage d'un ''geometry shader'', pour déterminer les primitives finales. Et il faut aussi refaire le ''culling'', au cas où les primitives générées ne soient pas visibles depuis la caméra. Heureusement, la sortie d'un ''geometry shader'' est soit un point, soit une ligne, soit un ''triangle strip'', ce qui simplifie la seconde phase d'assemblage des primitives. Avec les ''geometry shaders'', il y a donc deux phases d'assemblage des primitives : une phase avant, décrite dans la section précédente, et une seconde phase simplifiée après les ''geometry shaders''. Il n'y a pas que la phase d'assemblage de primitives qui est dupliquée : le tampon de primitives l'est aussi. On trouve donc un tampon de primitives à l'entrée des ''geometry shaders'' et un autre à la sortie. [[File:Implémentation matérielle des geometry shaders.png|centre|vignette|upright=2|Implémentation matérielle des geometry shaders]] L'implémentation des tampons de primitive est assez compliquée par la spécification des ''geometry shaders''. Un ''geometry shader'' fournit un résultat très variable en fonction de ses entrées. Pour une même entrée, la sortie peut aller d'une simple primitive à plusieurs dizaines. Le ''geometry shader'' précise cependant un nombre limite de sommets qu'il ne peut pas dépasser en sortie. Il peut ainsi préciser qu'il ne sortira pas plus de 16 sommets, par exemple. Mais ce nombre est généralement très élevé, bien plus que la moyenne réelle du résultat du ''geometry shader''. Or, le tampon de primitives de sortie a une taille finie qui doit être partagée entre plusieurs instances du ''geometry shader''. Et cette répartition n'est pas dynamique, mais statique : chaque instance reçoit une certaine portion du tampon de primitive, égale à la taille du tampon de primitives divisée par ce nombre limite. Aussi, le nombre d'instance exécutables en parallèle est rapidement limitée par le nombre de sommets maximal que peut sortir le ''geometry shader'', nombre qui est rarement atteint en pratique. ===La fonctionnalité de ''stream output''=== Une fonctionnalité des ''geometry shaders'' est la possibilité d'enregistrer leurs résultats en mémoire. Il s'agit de la fonctionnalité du '''''stream output'''''. On peut ainsi remplir une texture ou le ''vertex buffer'' dans la mémoire vidéo, avec le résultat d'un ''geometry shader''. Notons que celle-ci mémorise un ensemble de primitives, pas autre chose. Cette fonctionnalité est utilisée pour certains effets ou rendu bien précis, mais il faut avouer qu'elle n'est pas très souvent utilisée. Aussi, les concepteurs de cartes graphiques n'ont pas optimisé cette fonctionnalité au maximum. Le ''stream output'' n'a généralement pas accès prioritaire à la mémoire, comparé aux ROP, et n'a souvent accès qu'à une partie limitée de la bande passante mémoire. Notons qu'il existe deux formes de ''stream output'' : une qui permet aux ''vertex shader'' d'écrire dans une texture, l'autre qui permet aux ''geometry shaders'' de le faire. Notons que le ''stream output'' fournit un flux de primitives, pas de sommets, même pour le flux sortant d'un ''vertex shader''. En clair, beaucoup de sommets sont dupliqués et ont n'a pas d{{'}}''index buffer''. Les résultats du ''stream output'' sont donc assez lourds et prennent beaucoup de mémoire. [[File:Stream output.png|centre|vignette|upright=2.5|Stream output]] ==DirectX 12 : les ''mesh shaders''== [[File:D3D11 Pipeline.svg|vignette|upright=1|Pipeline graphique de Direct x 11.]] Avec l'introduction des ''geometry shaders'' et de la tesselation, le pipeline graphique est devenu très complexe. Plusieurs étages en plus sont ajoutés à sa portion géométrique : un pour les ''geometry shaders'', trois pour la tesselation, et ce en plus des ''vertex shaders'' existants et des étages non-programmables. Le pipeline en question est celui d'Open GL 4 et de DirectX 11. Mais Direct X 12 a simplifié le tout, sous l'impulsion de technologies introduites par AMD et de NVIDIA. AMD a introduit les ''primitive shaders'', NVIDIA a introduit les ''mesh shaders'''' ont été introduit par NVIDIA. Les derniers ont été gardés pour DirectX 12, simplifiant grandement le pipeline. ===Les primitive/mesh shaders=== Les deux solutions de AMD et NVIDIA partent du même principe : elles fusionnent certaines étapes du pipeline. Les ''primitive/mesh shaders'' font disparaitre les étapes d{{'}}''input assembly'' et d'assemblage de primitives, qui sont maintenant gérées par les ''primitive/mesh shaders''. Les ''primitive/mesh shaders'' lisent directement le tampon d'indice et lisent les sommets depuis la VRAM, sans passer par une étape non-programmable. Ils assemblent les primitives eux-mêmes et les envoient directement au rastériseur. Le tout permet des optimisations très intéressantes, comme un ''culling'' précoce. Les ''mesh shaders'' sont des ''shaders'' généralistes, semblables aux ''compute shaders''. Pour rappel, un ''compute shader'' peut lire des données en RAM, exécuter des traitements dessus, et enregistrer les résultats en RAM. Il peut lire ou écrire à des adresses arbitraires, sans limitations. Il n'est pas limité à lire des données consécutives, peut sauter d'une donnée à une autre donnée distante en RAM. Les ''mesh shaders'' sont des variantes des ''compute shaders'', qui n'écrivent pas leur résultat en RAM, mais envoient celui-ci au rastériseur. Plus précisément, ils écrivent leur résultat dans le tampon de primitives. Les ''mesh shaders'' peuvent contourner l'étape d{{'}}''input assembly'' et la remplacer par leur propre code. Pour rappel, l'étape d{{'}}''input assembly'' était non-programmable et gérait des tampons de vertices et d'indices très normés. Les sommets étaient lus soit un par un, soit par paquets de N sommets consécutifs, ce qui était assez rigide. Il n'y avait pas d'accès arbitraire en mémoire RAM comme peuvent le faire les ''compute shaders''. Par contre, un ''mesh shader'' peut accéder aux sommets de la manière qu'il souhaite, ce qui permet d'émuler un ''input assembler'' normal et plus encore. Une autre différence avec les ''vertex shaders'' est qu'ils ne traitent pas forcément des sommets, mais peuvent aussi envoyer des primitives au rastériseur directement. En clair, ils n'ont pas besoin d'une étape de ''primitive assembly'', qu'ils peuvent émuler directement dans le ''shader'' lui-même. Le ''culling'' est lui aussi réalisé par le ''primitive shader'', pas par une unité fixe. Et cela permet de contourner un problème fondamental des ''vertex shaders'' : il fallait que les primitives soient assemblées pour qu'on puisse déterminer si elles sont ou non invisibles. A l'opposé, les ''primitive/mesh shaders'' assemblent les primitives de manière précoce dans le ''primitive/mesh shader'', ce qui permet d'éliminer les primitives invisibles le plus tôt possible. Pour cela, les opérations permettant de déterminer si une primitive est visible sont exécutés en priorité, les autres opérations sont retardées et effectuées le plus tard possible. Ainsi, les calculs pour colorier ou orienter un sommet ne sont pas exécutés si le sommet est invisible. Il y a des différences entre ''primitive'' et ''mesh shaders''. Les ''primitive shaders'' permettent de lire un sommet à la fois, alors que les ''mesh shaders'' permettent de lire des ''batchs'' de plusieurs primitives d'un coup. Ces ''batchs'' de plusieurs primitives sont appelés des meshlets. La différence n'est pas fondamentale : le hardware des cartes AMD, qui gère des ''primitive shaders'', peut regrouper dynamiquement plusieurs instances de ''primitive shaders'' en un seul ''mesh shader'', via les technique de SIMT (une instance de ''primitive shader'' effectue des opérations scalaires, qui peuvent être regroupées en une seule instance SIMD en traitant plusieurs sommets en parallèle). La seule différence est que les ''mesh shaders'' exposent ce comportement au niveau du jeu d'instruction des ''shaders'', les programmeurs en ont conscience. ===Le pipeline géométrique avec les ''primitive/mesh shaders''=== Avec les ''primitive shaders'', l'implémentation exacte dépend de si la tesselation est activée ou non. Si la tesselation n'est pas activée, le ''vertex shader'' et le ''geométry shader'' sont fusionnés en un seul ''primitive shader''. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, sans tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="4" | |- ! DirectX 12 | colspan="4" | ''Primitive shader'' (AMD) |} Avec la tesselation activée, les ''geometry shaders'' et les ''domain shaders'' en un seul ''shader''. De même, les ''vertex shaders'' et les ''hull shaders'' sont fusionnés en un seul ''shader'', nommé l{{'}}''amplification shader''. Ainsi, le pipeline graphique est grandement simplifié, avec seulement deux ''shaders'' et un étage fixe, au lieu de quatre ''shaders'' différents. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, avec tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="7" | |- ! DirectX 12 | colspan="3" | * ''Amplification shader'' (AMD) | class="f_rouge" | Tesselation | colspan="3" | * ''Primitive shader'' (AMD) |} <noinclude> {{NavChapitre | book=Les cartes graphiques | prev=Le pipeline géométrique : évolution | prevText=Le pipeline géométrique : évolution | next=Le rasterizeur | nextText=Le rasterizeur }}{{autocat}} </noinclude> hfb6mcc5fmimqzkhep2119cupqfo9m2 763467 763466 2026-04-11T16:02:17Z Mewtow 31375 /* L'input assembler et l'assemblage de primitives */ 763467 wikitext text/x-wiki Dans le chapitre précédent, nous avons vu qu'il y a une différence entre le pipeline géométrique des anciennes stations de travail et des ordinateurs personnels. Les premiers tendaient à utiliser des processeurs flottants, programmés avec un ''firmware/microcode'' non-modifiable. Les ordinateurs personnels ont eu commencé avec des circuits géométriques fixe, pour les rendre de plus en plus programmables. Dans ce chapitre, nous allons étudier les circuits géométriques d'un GPU d'ordinateur personnel, et voir comment ils ont évolués dans le temps. ==Le ''vertex pipeline''== Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du cube de l'image ci-contre. Le sommet rouge du cube appartient aux 3 faces grise, jaune et bleue, et sera présent en trois exemplaires dans le tampon de sommets : un pour la face bleue, un pour la jaune, et un pour la grise. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : On pourrait remplacer les indices par des pointeurs, ce qui donnerait un cas particulier d'une structure de données connue sous le nom de vecteur de Liffe. Mais ce n'est pas très pratique et n'est pas utilisé dans le domaine du rendu 3D. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L'''input assembler'' et le tampon d'indice=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Pour faire son travail, il a besoin de l'adresse des données géométriques en mémoire, leur taille et éventuellement du type des données qu'on lui envoie (sommets codées sur 32 bits, 64, 128, etc). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. Et en général, c'est une unité d'accès mémoire un peu particulière, qui contient des circuits assez classiques pour ce genre de circuits : des circuits de calcul d'adresse, des circuits pour commander la mémoire VRAM, un contrôleur mémoire, diverses mémoires tampons, etc. Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques. [[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]] Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela. Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice. Les unités de ''index fetch'' et de ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances. Vous remarquerez que l’''input assembler'' fait surtout des calculs d'adresse, des lectures en mémoire, et des conversions de format de données. Un processeur de ''vertex shader'' peut faire la même chose, ce qui fait qu'il est possible d'émuler l'''input assembler'' avec un ''vertex shader''. La seule condition, absolument nécessaire, est que le ''vertex shader'' puisse lire des données en mémoire vidéo. Et pas seulement lire des textures, comme le permettent les techniques de ''vertex texturing'', mais de vraies lectures arbitraires, pour lire les tampons de sommet/indice. Cette possibilité est arrivée avec Direct X 10, ce qui fait que l’''input assembler'' peut être émulé par les ''vertex shaders'' à partir de cette version de Direct X. De nos jours, tous les GPUs font à leur sauce. Certains émulent l’''input assembler'' avec des ''shaders'', d'autres non. Ceux qui le font le font en modifiant les ''vertex shaders''. Le ''driver'' du GPU injecte du code dans les ''vertex shaders'', code qui émule l'''input assembler''. ===Les caches de sommets : une optimisation du tampon d'indice=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais quand des sommets sont dupliqués, ce n'est pas le cas. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Elle est appelée le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants. L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''. Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''. Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide. Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite. [[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]] Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances. [[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]] Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'assemblage de primitives=== En sortie des unités géométriques, on a des sommets éclairés et colorisés, pas des triangles. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. Un problème pour l'assemblage de primitives est que les sommets n’arrivent pas dans l'ordre. Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées. Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle. Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles. Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. ==L'étape de T&L et les vertex shaders== L'''input assembler'' est suivi par une étape de '''transformation-projection'''. Elle regroupe plusieurs manipulations différentes, mais qui 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 3D du point de vue de la caméra, et un autre qui corrige la perspective. Un changement de coordonnée s'effectue assez 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 petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes, et que 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 de détail. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée unité de T&L (Transform & Lighting). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'input assembler, ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. L'unité de T&L est devenue programmable dès la Geforce 3, première carte graphique à supporter les vertex shaders. Il y a peu de généralités spécifiques pour les processeurs de vertex shaders, tout a déjà été dit dans le chapitre sur les processeurs de shaders. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des vertex shaders, distincts des processeurs pour les pixel shaders. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de vertex shader d’antan. ==DirectX 10 : les ''geometry shaders''== Les GPU d'avant DirectX 10, qui n'avaient que les ''vertex shaders'' et ne pouvaient manipuler que des sommets. Depuis DirectX 10, le pipeline graphique a intégré des techniques pour gérer nativement des triangles dans les ''shaders''. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. L'intérêt est que cela permet de faciliter l'implémentation de techniques de tesselation, sans compter que certaines optimisations deviennent plus simples à effectuer. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. DirectX 10 et OpenGl 3.2 ont introduit les ''geometry shaders'', juste avant l'étape d'assemblage des primitives. Les ''geometry shaders'' peuvent ajouter, supprimer ou altérer des primitives dans une scène 3D. Un ''geometry shader'' prend en entrée un point, une ligne ou un triangle, donc les trois primitives de base supportées sur les GPU modernes. Il émet en sortie : soit un ''triangle strip'', soit une ''line strip'' (c'est à une ligne ce qu'un d'un ''triangle strip'' est à un triangle) ou un point. Ils n'ont pas été très utilisés, leurs utilisations étant assez limitées. Ils peuvent en théorie être utilisés pour la gestion des ''cubemaps'', le ''shadow volume extrusion'', la génération de particules, et quelques autres effets graphiques. Ils pourraient aussi être utilisés pour faire de la tesselation, mais leurs limitations font que ce n'est pas pratique. Rappelons que les ''geometry shaders'' sont optionnels et que beaucoup de jeux vidéos ou de moteurs de rendu 3D n'en utilisent pas. ===L'étape d’assemblage de primitives est dupliquée=== Les ''geometry shaders'' n'ont jamais eu de processeur de shader dédié, car ils ont été introduits avec DirectX 10 et OpenGl 3.2, en même temps que les processeurs de ''shaders'' ont étés unifiés (rendu capable d’exécuter n'importe quel ''shader''). Leur place dans le pipeline graphique est quelque peu étrange. En théorie, ils sont placés après l'assembleur de primitive, car ils manipulent les primitives fournies par l'étape d'assemblage des primitives. Mais le résultat fournit par les ''geometry shaders'' doit être retraité par l'assembleur de primitive. En effet, j'ai menti plus haut en disant que les ''geometry shaders'' fournissent en entrée de 0 à plusieurs primitives : la sortie d'un ''geometry shader'' est un ensemble de sommets, non-regroupés en primitives. Le résultat est que l'assembleur de primitive doit refaire son travail après le passage d'un ''geometry shader'', pour déterminer les primitives finales. Et il faut aussi refaire le ''culling'', au cas où les primitives générées ne soient pas visibles depuis la caméra. Heureusement, la sortie d'un ''geometry shader'' est soit un point, soit une ligne, soit un ''triangle strip'', ce qui simplifie la seconde phase d'assemblage des primitives. Avec les ''geometry shaders'', il y a donc deux phases d'assemblage des primitives : une phase avant, décrite dans la section précédente, et une seconde phase simplifiée après les ''geometry shaders''. Il n'y a pas que la phase d'assemblage de primitives qui est dupliquée : le tampon de primitives l'est aussi. On trouve donc un tampon de primitives à l'entrée des ''geometry shaders'' et un autre à la sortie. [[File:Implémentation matérielle des geometry shaders.png|centre|vignette|upright=2|Implémentation matérielle des geometry shaders]] L'implémentation des tampons de primitive est assez compliquée par la spécification des ''geometry shaders''. Un ''geometry shader'' fournit un résultat très variable en fonction de ses entrées. Pour une même entrée, la sortie peut aller d'une simple primitive à plusieurs dizaines. Le ''geometry shader'' précise cependant un nombre limite de sommets qu'il ne peut pas dépasser en sortie. Il peut ainsi préciser qu'il ne sortira pas plus de 16 sommets, par exemple. Mais ce nombre est généralement très élevé, bien plus que la moyenne réelle du résultat du ''geometry shader''. Or, le tampon de primitives de sortie a une taille finie qui doit être partagée entre plusieurs instances du ''geometry shader''. Et cette répartition n'est pas dynamique, mais statique : chaque instance reçoit une certaine portion du tampon de primitive, égale à la taille du tampon de primitives divisée par ce nombre limite. Aussi, le nombre d'instance exécutables en parallèle est rapidement limitée par le nombre de sommets maximal que peut sortir le ''geometry shader'', nombre qui est rarement atteint en pratique. ===La fonctionnalité de ''stream output''=== Une fonctionnalité des ''geometry shaders'' est la possibilité d'enregistrer leurs résultats en mémoire. Il s'agit de la fonctionnalité du '''''stream output'''''. On peut ainsi remplir une texture ou le ''vertex buffer'' dans la mémoire vidéo, avec le résultat d'un ''geometry shader''. Notons que celle-ci mémorise un ensemble de primitives, pas autre chose. Cette fonctionnalité est utilisée pour certains effets ou rendu bien précis, mais il faut avouer qu'elle n'est pas très souvent utilisée. Aussi, les concepteurs de cartes graphiques n'ont pas optimisé cette fonctionnalité au maximum. Le ''stream output'' n'a généralement pas accès prioritaire à la mémoire, comparé aux ROP, et n'a souvent accès qu'à une partie limitée de la bande passante mémoire. Notons qu'il existe deux formes de ''stream output'' : une qui permet aux ''vertex shader'' d'écrire dans une texture, l'autre qui permet aux ''geometry shaders'' de le faire. Notons que le ''stream output'' fournit un flux de primitives, pas de sommets, même pour le flux sortant d'un ''vertex shader''. En clair, beaucoup de sommets sont dupliqués et ont n'a pas d{{'}}''index buffer''. Les résultats du ''stream output'' sont donc assez lourds et prennent beaucoup de mémoire. [[File:Stream output.png|centre|vignette|upright=2.5|Stream output]] ==DirectX 12 : les ''mesh shaders''== [[File:D3D11 Pipeline.svg|vignette|upright=1|Pipeline graphique de Direct x 11.]] Avec l'introduction des ''geometry shaders'' et de la tesselation, le pipeline graphique est devenu très complexe. Plusieurs étages en plus sont ajoutés à sa portion géométrique : un pour les ''geometry shaders'', trois pour la tesselation, et ce en plus des ''vertex shaders'' existants et des étages non-programmables. Le pipeline en question est celui d'Open GL 4 et de DirectX 11. Mais Direct X 12 a simplifié le tout, sous l'impulsion de technologies introduites par AMD et de NVIDIA. AMD a introduit les ''primitive shaders'', NVIDIA a introduit les ''mesh shaders'''' ont été introduit par NVIDIA. Les derniers ont été gardés pour DirectX 12, simplifiant grandement le pipeline. ===Les primitive/mesh shaders=== Les deux solutions de AMD et NVIDIA partent du même principe : elles fusionnent certaines étapes du pipeline. Les ''primitive/mesh shaders'' font disparaitre les étapes d{{'}}''input assembly'' et d'assemblage de primitives, qui sont maintenant gérées par les ''primitive/mesh shaders''. Les ''primitive/mesh shaders'' lisent directement le tampon d'indice et lisent les sommets depuis la VRAM, sans passer par une étape non-programmable. Ils assemblent les primitives eux-mêmes et les envoient directement au rastériseur. Le tout permet des optimisations très intéressantes, comme un ''culling'' précoce. Les ''mesh shaders'' sont des ''shaders'' généralistes, semblables aux ''compute shaders''. Pour rappel, un ''compute shader'' peut lire des données en RAM, exécuter des traitements dessus, et enregistrer les résultats en RAM. Il peut lire ou écrire à des adresses arbitraires, sans limitations. Il n'est pas limité à lire des données consécutives, peut sauter d'une donnée à une autre donnée distante en RAM. Les ''mesh shaders'' sont des variantes des ''compute shaders'', qui n'écrivent pas leur résultat en RAM, mais envoient celui-ci au rastériseur. Plus précisément, ils écrivent leur résultat dans le tampon de primitives. Les ''mesh shaders'' peuvent contourner l'étape d{{'}}''input assembly'' et la remplacer par leur propre code. Pour rappel, l'étape d{{'}}''input assembly'' était non-programmable et gérait des tampons de vertices et d'indices très normés. Les sommets étaient lus soit un par un, soit par paquets de N sommets consécutifs, ce qui était assez rigide. Il n'y avait pas d'accès arbitraire en mémoire RAM comme peuvent le faire les ''compute shaders''. Par contre, un ''mesh shader'' peut accéder aux sommets de la manière qu'il souhaite, ce qui permet d'émuler un ''input assembler'' normal et plus encore. Une autre différence avec les ''vertex shaders'' est qu'ils ne traitent pas forcément des sommets, mais peuvent aussi envoyer des primitives au rastériseur directement. En clair, ils n'ont pas besoin d'une étape de ''primitive assembly'', qu'ils peuvent émuler directement dans le ''shader'' lui-même. Le ''culling'' est lui aussi réalisé par le ''primitive shader'', pas par une unité fixe. Et cela permet de contourner un problème fondamental des ''vertex shaders'' : il fallait que les primitives soient assemblées pour qu'on puisse déterminer si elles sont ou non invisibles. A l'opposé, les ''primitive/mesh shaders'' assemblent les primitives de manière précoce dans le ''primitive/mesh shader'', ce qui permet d'éliminer les primitives invisibles le plus tôt possible. Pour cela, les opérations permettant de déterminer si une primitive est visible sont exécutés en priorité, les autres opérations sont retardées et effectuées le plus tard possible. Ainsi, les calculs pour colorier ou orienter un sommet ne sont pas exécutés si le sommet est invisible. Il y a des différences entre ''primitive'' et ''mesh shaders''. Les ''primitive shaders'' permettent de lire un sommet à la fois, alors que les ''mesh shaders'' permettent de lire des ''batchs'' de plusieurs primitives d'un coup. Ces ''batchs'' de plusieurs primitives sont appelés des meshlets. La différence n'est pas fondamentale : le hardware des cartes AMD, qui gère des ''primitive shaders'', peut regrouper dynamiquement plusieurs instances de ''primitive shaders'' en un seul ''mesh shader'', via les technique de SIMT (une instance de ''primitive shader'' effectue des opérations scalaires, qui peuvent être regroupées en une seule instance SIMD en traitant plusieurs sommets en parallèle). La seule différence est que les ''mesh shaders'' exposent ce comportement au niveau du jeu d'instruction des ''shaders'', les programmeurs en ont conscience. ===Le pipeline géométrique avec les ''primitive/mesh shaders''=== Avec les ''primitive shaders'', l'implémentation exacte dépend de si la tesselation est activée ou non. Si la tesselation n'est pas activée, le ''vertex shader'' et le ''geométry shader'' sont fusionnés en un seul ''primitive shader''. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, sans tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="4" | |- ! DirectX 12 | colspan="4" | ''Primitive shader'' (AMD) |} Avec la tesselation activée, les ''geometry shaders'' et les ''domain shaders'' en un seul ''shader''. De même, les ''vertex shaders'' et les ''hull shaders'' sont fusionnés en un seul ''shader'', nommé l{{'}}''amplification shader''. Ainsi, le pipeline graphique est grandement simplifié, avec seulement deux ''shaders'' et un étage fixe, au lieu de quatre ''shaders'' différents. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, avec tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="7" | |- ! DirectX 12 | colspan="3" | * ''Amplification shader'' (AMD) | class="f_rouge" | Tesselation | colspan="3" | * ''Primitive shader'' (AMD) |} <noinclude> {{NavChapitre | book=Les cartes graphiques | prev=Le pipeline géométrique : évolution | prevText=Le pipeline géométrique : évolution | next=Le rasterizeur | nextText=Le rasterizeur }}{{autocat}} </noinclude> g4bkcp3nqrzwbo9d0kbvacc4a4jloor 763468 763467 2026-04-11T16:04:48Z Mewtow 31375 /* Le vertex pipeline */ 763468 wikitext text/x-wiki Dans le chapitre précédent, nous avons vu qu'il y a une différence entre le pipeline géométrique des anciennes stations de travail et des ordinateurs personnels. Les premiers tendaient à utiliser des processeurs flottants, programmés avec un ''firmware/microcode'' non-modifiable. Les ordinateurs personnels ont eu commencé avec des circuits géométriques fixe, pour les rendre de plus en plus programmables. Dans ce chapitre, nous allons étudier les circuits géométriques d'un GPU d'ordinateur personnel, et voir comment ils ont évolués dans le temps. ==Le ''vertex pipeline''== Les premières cartes graphiques ne traitaient que des sommets, les primitives n'apparaissaient qu'à l'étape de rastérisation. Leur pipeline a progressivement évolué pour pouvoir exécuter des ''shaders'' sur des primitives, mais ce n'est apparu qu'avec DirectX 10. Avant, les unités géométriques ne géraient que des sommets. Nous allons voir de telles unités géométriques ici. Elles sont composées de trois circuits : l'''input assembly'', l'unité géométrique proprement dit, et l'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | class="f_rouge" | ''Input assembly'' | ''Transform & Lighting'' ou ''Vertex shader'' | class="f_rouge" | ''Primitive assembly'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du cube de l'image ci-contre. Le sommet rouge du cube appartient aux 3 faces grise, jaune et bleue, et sera présent en trois exemplaires dans le tampon de sommets : un pour la face bleue, un pour la jaune, et un pour la grise. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : On pourrait remplacer les indices par des pointeurs, ce qui donnerait un cas particulier d'une structure de données connue sous le nom de vecteur de Liffe. Mais ce n'est pas très pratique et n'est pas utilisé dans le domaine du rendu 3D. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L'''input assembler'' et le tampon d'indice=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Pour faire son travail, il a besoin de l'adresse des données géométriques en mémoire, leur taille et éventuellement du type des données qu'on lui envoie (sommets codées sur 32 bits, 64, 128, etc). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. Et en général, c'est une unité d'accès mémoire un peu particulière, qui contient des circuits assez classiques pour ce genre de circuits : des circuits de calcul d'adresse, des circuits pour commander la mémoire VRAM, un contrôleur mémoire, diverses mémoires tampons, etc. Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques. [[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]] Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela. Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice. Les unités de ''index fetch'' et de ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances. Vous remarquerez que l’''input assembler'' fait surtout des calculs d'adresse, des lectures en mémoire, et des conversions de format de données. Un processeur de ''vertex shader'' peut faire la même chose, ce qui fait qu'il est possible d'émuler l'''input assembler'' avec un ''vertex shader''. La seule condition, absolument nécessaire, est que le ''vertex shader'' puisse lire des données en mémoire vidéo. Et pas seulement lire des textures, comme le permettent les techniques de ''vertex texturing'', mais de vraies lectures arbitraires, pour lire les tampons de sommet/indice. Cette possibilité est arrivée avec Direct X 10, ce qui fait que l’''input assembler'' peut être émulé par les ''vertex shaders'' à partir de cette version de Direct X. De nos jours, tous les GPUs font à leur sauce. Certains émulent l’''input assembler'' avec des ''shaders'', d'autres non. Ceux qui le font le font en modifiant les ''vertex shaders''. Le ''driver'' du GPU injecte du code dans les ''vertex shaders'', code qui émule l'''input assembler''. ===Les caches de sommets : une optimisation du tampon d'indice=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais quand des sommets sont dupliqués, ce n'est pas le cas. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Elle est appelée le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants. L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''. Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''. Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide. Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite. [[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]] Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances. [[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]] Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'assemblage de primitives=== En sortie des unités géométriques, on a des sommets éclairés et colorisés, pas des triangles. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. Un problème pour l'assemblage de primitives est que les sommets n’arrivent pas dans l'ordre. Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées. Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle. Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles. Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. ==L'étape de T&L et les vertex shaders== L'''input assembler'' est suivi par une étape de '''transformation-projection'''. Elle regroupe plusieurs manipulations différentes, mais qui 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 3D du point de vue de la caméra, et un autre qui corrige la perspective. Un changement de coordonnée s'effectue assez 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 petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes, et que 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 de détail. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée unité de T&L (Transform & Lighting). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'input assembler, ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. L'unité de T&L est devenue programmable dès la Geforce 3, première carte graphique à supporter les vertex shaders. Il y a peu de généralités spécifiques pour les processeurs de vertex shaders, tout a déjà été dit dans le chapitre sur les processeurs de shaders. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des vertex shaders, distincts des processeurs pour les pixel shaders. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de vertex shader d’antan. ==DirectX 10 : les ''geometry shaders''== Les GPU d'avant DirectX 10, qui n'avaient que les ''vertex shaders'' et ne pouvaient manipuler que des sommets. Depuis DirectX 10, le pipeline graphique a intégré des techniques pour gérer nativement des triangles dans les ''shaders''. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. L'intérêt est que cela permet de faciliter l'implémentation de techniques de tesselation, sans compter que certaines optimisations deviennent plus simples à effectuer. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. DirectX 10 et OpenGl 3.2 ont introduit les ''geometry shaders'', juste avant l'étape d'assemblage des primitives. Les ''geometry shaders'' peuvent ajouter, supprimer ou altérer des primitives dans une scène 3D. Un ''geometry shader'' prend en entrée un point, une ligne ou un triangle, donc les trois primitives de base supportées sur les GPU modernes. Il émet en sortie : soit un ''triangle strip'', soit une ''line strip'' (c'est à une ligne ce qu'un d'un ''triangle strip'' est à un triangle) ou un point. Ils n'ont pas été très utilisés, leurs utilisations étant assez limitées. Ils peuvent en théorie être utilisés pour la gestion des ''cubemaps'', le ''shadow volume extrusion'', la génération de particules, et quelques autres effets graphiques. Ils pourraient aussi être utilisés pour faire de la tesselation, mais leurs limitations font que ce n'est pas pratique. Rappelons que les ''geometry shaders'' sont optionnels et que beaucoup de jeux vidéos ou de moteurs de rendu 3D n'en utilisent pas. ===L'étape d’assemblage de primitives est dupliquée=== Les ''geometry shaders'' n'ont jamais eu de processeur de shader dédié, car ils ont été introduits avec DirectX 10 et OpenGl 3.2, en même temps que les processeurs de ''shaders'' ont étés unifiés (rendu capable d’exécuter n'importe quel ''shader''). Leur place dans le pipeline graphique est quelque peu étrange. En théorie, ils sont placés après l'assembleur de primitive, car ils manipulent les primitives fournies par l'étape d'assemblage des primitives. Mais le résultat fournit par les ''geometry shaders'' doit être retraité par l'assembleur de primitive. En effet, j'ai menti plus haut en disant que les ''geometry shaders'' fournissent en entrée de 0 à plusieurs primitives : la sortie d'un ''geometry shader'' est un ensemble de sommets, non-regroupés en primitives. Le résultat est que l'assembleur de primitive doit refaire son travail après le passage d'un ''geometry shader'', pour déterminer les primitives finales. Et il faut aussi refaire le ''culling'', au cas où les primitives générées ne soient pas visibles depuis la caméra. Heureusement, la sortie d'un ''geometry shader'' est soit un point, soit une ligne, soit un ''triangle strip'', ce qui simplifie la seconde phase d'assemblage des primitives. Avec les ''geometry shaders'', il y a donc deux phases d'assemblage des primitives : une phase avant, décrite dans la section précédente, et une seconde phase simplifiée après les ''geometry shaders''. Il n'y a pas que la phase d'assemblage de primitives qui est dupliquée : le tampon de primitives l'est aussi. On trouve donc un tampon de primitives à l'entrée des ''geometry shaders'' et un autre à la sortie. [[File:Implémentation matérielle des geometry shaders.png|centre|vignette|upright=2|Implémentation matérielle des geometry shaders]] L'implémentation des tampons de primitive est assez compliquée par la spécification des ''geometry shaders''. Un ''geometry shader'' fournit un résultat très variable en fonction de ses entrées. Pour une même entrée, la sortie peut aller d'une simple primitive à plusieurs dizaines. Le ''geometry shader'' précise cependant un nombre limite de sommets qu'il ne peut pas dépasser en sortie. Il peut ainsi préciser qu'il ne sortira pas plus de 16 sommets, par exemple. Mais ce nombre est généralement très élevé, bien plus que la moyenne réelle du résultat du ''geometry shader''. Or, le tampon de primitives de sortie a une taille finie qui doit être partagée entre plusieurs instances du ''geometry shader''. Et cette répartition n'est pas dynamique, mais statique : chaque instance reçoit une certaine portion du tampon de primitive, égale à la taille du tampon de primitives divisée par ce nombre limite. Aussi, le nombre d'instance exécutables en parallèle est rapidement limitée par le nombre de sommets maximal que peut sortir le ''geometry shader'', nombre qui est rarement atteint en pratique. ===La fonctionnalité de ''stream output''=== Une fonctionnalité des ''geometry shaders'' est la possibilité d'enregistrer leurs résultats en mémoire. Il s'agit de la fonctionnalité du '''''stream output'''''. On peut ainsi remplir une texture ou le ''vertex buffer'' dans la mémoire vidéo, avec le résultat d'un ''geometry shader''. Notons que celle-ci mémorise un ensemble de primitives, pas autre chose. Cette fonctionnalité est utilisée pour certains effets ou rendu bien précis, mais il faut avouer qu'elle n'est pas très souvent utilisée. Aussi, les concepteurs de cartes graphiques n'ont pas optimisé cette fonctionnalité au maximum. Le ''stream output'' n'a généralement pas accès prioritaire à la mémoire, comparé aux ROP, et n'a souvent accès qu'à une partie limitée de la bande passante mémoire. Notons qu'il existe deux formes de ''stream output'' : une qui permet aux ''vertex shader'' d'écrire dans une texture, l'autre qui permet aux ''geometry shaders'' de le faire. Notons que le ''stream output'' fournit un flux de primitives, pas de sommets, même pour le flux sortant d'un ''vertex shader''. En clair, beaucoup de sommets sont dupliqués et ont n'a pas d{{'}}''index buffer''. Les résultats du ''stream output'' sont donc assez lourds et prennent beaucoup de mémoire. [[File:Stream output.png|centre|vignette|upright=2.5|Stream output]] ==DirectX 12 : les ''mesh shaders''== [[File:D3D11 Pipeline.svg|vignette|upright=1|Pipeline graphique de Direct x 11.]] Avec l'introduction des ''geometry shaders'' et de la tesselation, le pipeline graphique est devenu très complexe. Plusieurs étages en plus sont ajoutés à sa portion géométrique : un pour les ''geometry shaders'', trois pour la tesselation, et ce en plus des ''vertex shaders'' existants et des étages non-programmables. Le pipeline en question est celui d'Open GL 4 et de DirectX 11. Mais Direct X 12 a simplifié le tout, sous l'impulsion de technologies introduites par AMD et de NVIDIA. AMD a introduit les ''primitive shaders'', NVIDIA a introduit les ''mesh shaders'''' ont été introduit par NVIDIA. Les derniers ont été gardés pour DirectX 12, simplifiant grandement le pipeline. ===Les primitive/mesh shaders=== Les deux solutions de AMD et NVIDIA partent du même principe : elles fusionnent certaines étapes du pipeline. Les ''primitive/mesh shaders'' font disparaitre les étapes d{{'}}''input assembly'' et d'assemblage de primitives, qui sont maintenant gérées par les ''primitive/mesh shaders''. Les ''primitive/mesh shaders'' lisent directement le tampon d'indice et lisent les sommets depuis la VRAM, sans passer par une étape non-programmable. Ils assemblent les primitives eux-mêmes et les envoient directement au rastériseur. Le tout permet des optimisations très intéressantes, comme un ''culling'' précoce. Les ''mesh shaders'' sont des ''shaders'' généralistes, semblables aux ''compute shaders''. Pour rappel, un ''compute shader'' peut lire des données en RAM, exécuter des traitements dessus, et enregistrer les résultats en RAM. Il peut lire ou écrire à des adresses arbitraires, sans limitations. Il n'est pas limité à lire des données consécutives, peut sauter d'une donnée à une autre donnée distante en RAM. Les ''mesh shaders'' sont des variantes des ''compute shaders'', qui n'écrivent pas leur résultat en RAM, mais envoient celui-ci au rastériseur. Plus précisément, ils écrivent leur résultat dans le tampon de primitives. Les ''mesh shaders'' peuvent contourner l'étape d{{'}}''input assembly'' et la remplacer par leur propre code. Pour rappel, l'étape d{{'}}''input assembly'' était non-programmable et gérait des tampons de vertices et d'indices très normés. Les sommets étaient lus soit un par un, soit par paquets de N sommets consécutifs, ce qui était assez rigide. Il n'y avait pas d'accès arbitraire en mémoire RAM comme peuvent le faire les ''compute shaders''. Par contre, un ''mesh shader'' peut accéder aux sommets de la manière qu'il souhaite, ce qui permet d'émuler un ''input assembler'' normal et plus encore. Une autre différence avec les ''vertex shaders'' est qu'ils ne traitent pas forcément des sommets, mais peuvent aussi envoyer des primitives au rastériseur directement. En clair, ils n'ont pas besoin d'une étape de ''primitive assembly'', qu'ils peuvent émuler directement dans le ''shader'' lui-même. Le ''culling'' est lui aussi réalisé par le ''primitive shader'', pas par une unité fixe. Et cela permet de contourner un problème fondamental des ''vertex shaders'' : il fallait que les primitives soient assemblées pour qu'on puisse déterminer si elles sont ou non invisibles. A l'opposé, les ''primitive/mesh shaders'' assemblent les primitives de manière précoce dans le ''primitive/mesh shader'', ce qui permet d'éliminer les primitives invisibles le plus tôt possible. Pour cela, les opérations permettant de déterminer si une primitive est visible sont exécutés en priorité, les autres opérations sont retardées et effectuées le plus tard possible. Ainsi, les calculs pour colorier ou orienter un sommet ne sont pas exécutés si le sommet est invisible. Il y a des différences entre ''primitive'' et ''mesh shaders''. Les ''primitive shaders'' permettent de lire un sommet à la fois, alors que les ''mesh shaders'' permettent de lire des ''batchs'' de plusieurs primitives d'un coup. Ces ''batchs'' de plusieurs primitives sont appelés des meshlets. La différence n'est pas fondamentale : le hardware des cartes AMD, qui gère des ''primitive shaders'', peut regrouper dynamiquement plusieurs instances de ''primitive shaders'' en un seul ''mesh shader'', via les technique de SIMT (une instance de ''primitive shader'' effectue des opérations scalaires, qui peuvent être regroupées en une seule instance SIMD en traitant plusieurs sommets en parallèle). La seule différence est que les ''mesh shaders'' exposent ce comportement au niveau du jeu d'instruction des ''shaders'', les programmeurs en ont conscience. ===Le pipeline géométrique avec les ''primitive/mesh shaders''=== Avec les ''primitive shaders'', l'implémentation exacte dépend de si la tesselation est activée ou non. Si la tesselation n'est pas activée, le ''vertex shader'' et le ''geométry shader'' sont fusionnés en un seul ''primitive shader''. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, sans tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="4" | |- ! DirectX 12 | colspan="4" | ''Primitive shader'' (AMD) |} Avec la tesselation activée, les ''geometry shaders'' et les ''domain shaders'' en un seul ''shader''. De même, les ''vertex shaders'' et les ''hull shaders'' sont fusionnés en un seul ''shader'', nommé l{{'}}''amplification shader''. Ainsi, le pipeline graphique est grandement simplifié, avec seulement deux ''shaders'' et un étage fixe, au lieu de quatre ''shaders'' différents. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, avec tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="7" | |- ! DirectX 12 | colspan="3" | * ''Amplification shader'' (AMD) | class="f_rouge" | Tesselation | colspan="3" | * ''Primitive shader'' (AMD) |} <noinclude> {{NavChapitre | book=Les cartes graphiques | prev=Le pipeline géométrique : évolution | prevText=Le pipeline géométrique : évolution | next=Le rasterizeur | nextText=Le rasterizeur }}{{autocat}} </noinclude> 90e3fcf9b5ya8svruvyzjrvipazecm8 763469 763468 2026-04-11T16:05:23Z Mewtow 31375 /* Le vertex pipeline */ 763469 wikitext text/x-wiki Dans le chapitre précédent, nous avons vu qu'il y a une différence entre le pipeline géométrique des anciennes stations de travail et des ordinateurs personnels. Les premiers tendaient à utiliser des processeurs flottants, programmés avec un ''firmware/microcode'' non-modifiable. Les ordinateurs personnels ont eu commencé avec des circuits géométriques fixe, pour les rendre de plus en plus programmables. Dans ce chapitre, nous allons étudier les circuits géométriques d'un GPU d'ordinateur personnel, et voir comment ils ont évolués dans le temps. ==Le ''vertex pipeline''== Les premières cartes graphiques ne traitaient que des sommets, les primitives n'apparaissaient qu'à l'étape de rastérisation. Leur pipeline a progressivement évolué pour pouvoir exécuter des ''shaders'' sur des primitives, mais ce n'est apparu qu'avec DirectX 10. Avant, les unités géométriques ne géraient que des sommets. Nous allons voir de telles unités géométriques ici. Elles sont composées de trois circuits : l'''input assembly'', l'unité géométrique proprement dit, et l'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Input assembly'' | ''Transform & Lighting'' | rowspan="2" class="f_rouge" | ''Primitive assembly'' |- | ''Vertex shader'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du cube de l'image ci-contre. Le sommet rouge du cube appartient aux 3 faces grise, jaune et bleue, et sera présent en trois exemplaires dans le tampon de sommets : un pour la face bleue, un pour la jaune, et un pour la grise. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : On pourrait remplacer les indices par des pointeurs, ce qui donnerait un cas particulier d'une structure de données connue sous le nom de vecteur de Liffe. Mais ce n'est pas très pratique et n'est pas utilisé dans le domaine du rendu 3D. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L'''input assembler'' et le tampon d'indice=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Pour faire son travail, il a besoin de l'adresse des données géométriques en mémoire, leur taille et éventuellement du type des données qu'on lui envoie (sommets codées sur 32 bits, 64, 128, etc). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. Et en général, c'est une unité d'accès mémoire un peu particulière, qui contient des circuits assez classiques pour ce genre de circuits : des circuits de calcul d'adresse, des circuits pour commander la mémoire VRAM, un contrôleur mémoire, diverses mémoires tampons, etc. Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques. [[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]] Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela. Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice. Les unités de ''index fetch'' et de ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances. Vous remarquerez que l’''input assembler'' fait surtout des calculs d'adresse, des lectures en mémoire, et des conversions de format de données. Un processeur de ''vertex shader'' peut faire la même chose, ce qui fait qu'il est possible d'émuler l'''input assembler'' avec un ''vertex shader''. La seule condition, absolument nécessaire, est que le ''vertex shader'' puisse lire des données en mémoire vidéo. Et pas seulement lire des textures, comme le permettent les techniques de ''vertex texturing'', mais de vraies lectures arbitraires, pour lire les tampons de sommet/indice. Cette possibilité est arrivée avec Direct X 10, ce qui fait que l’''input assembler'' peut être émulé par les ''vertex shaders'' à partir de cette version de Direct X. De nos jours, tous les GPUs font à leur sauce. Certains émulent l’''input assembler'' avec des ''shaders'', d'autres non. Ceux qui le font le font en modifiant les ''vertex shaders''. Le ''driver'' du GPU injecte du code dans les ''vertex shaders'', code qui émule l'''input assembler''. ===Les caches de sommets : une optimisation du tampon d'indice=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais quand des sommets sont dupliqués, ce n'est pas le cas. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Elle est appelée le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants. L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''. Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''. Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide. Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite. [[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]] Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances. [[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]] Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'assemblage de primitives=== En sortie des unités géométriques, on a des sommets éclairés et colorisés, pas des triangles. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. Un problème pour l'assemblage de primitives est que les sommets n’arrivent pas dans l'ordre. Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées. Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle. Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles. Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. ==L'étape de T&L et les vertex shaders== L'''input assembler'' est suivi par une étape de '''transformation-projection'''. Elle regroupe plusieurs manipulations différentes, mais qui 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 3D du point de vue de la caméra, et un autre qui corrige la perspective. Un changement de coordonnée s'effectue assez 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 petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes, et que 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 de détail. L'étape de transformation est souvent fusionnée avec l'étape d'éclairage par sommet, les deux demandant de faire des calculs assez similaires. Sur PC, la geforce 256 a été la première à intégrer une unité non-programmable pour gérer transformation et éclairage, appelée unité de T&L (Transform & Lighting). L'unité de T&L incorporait un ou plusieurs circuits de multiplication de matrices spécialisés pour l'étape de transformation. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Elles prennent en entrée les données provenant de l'input assembler, ainsi que les matrices nécessaires, et fournissent en sortie les coordonnées des sommets transformés. L'unité de T&L est devenue programmable dès la Geforce 3, première carte graphique à supporter les vertex shaders. Il y a peu de généralités spécifiques pour les processeurs de vertex shaders, tout a déjà été dit dans le chapitre sur les processeurs de shaders. Tout au plus peut on dire que les cartes graphiques avaient autrefois des processeurs spécialisés dans l’exécution des vertex shaders, distincts des processeurs pour les pixel shaders. Mais cette époque est révolue avec les cartes graphiques actuelles. Par contre, on peut étudier un exemple de processeur de vertex shader d’antan. ==DirectX 10 : les ''geometry shaders''== Les GPU d'avant DirectX 10, qui n'avaient que les ''vertex shaders'' et ne pouvaient manipuler que des sommets. Depuis DirectX 10, le pipeline graphique a intégré des techniques pour gérer nativement des triangles dans les ''shaders''. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. L'intérêt est que cela permet de faciliter l'implémentation de techniques de tesselation, sans compter que certaines optimisations deviennent plus simples à effectuer. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. DirectX 10 et OpenGl 3.2 ont introduit les ''geometry shaders'', juste avant l'étape d'assemblage des primitives. Les ''geometry shaders'' peuvent ajouter, supprimer ou altérer des primitives dans une scène 3D. Un ''geometry shader'' prend en entrée un point, une ligne ou un triangle, donc les trois primitives de base supportées sur les GPU modernes. Il émet en sortie : soit un ''triangle strip'', soit une ''line strip'' (c'est à une ligne ce qu'un d'un ''triangle strip'' est à un triangle) ou un point. Ils n'ont pas été très utilisés, leurs utilisations étant assez limitées. Ils peuvent en théorie être utilisés pour la gestion des ''cubemaps'', le ''shadow volume extrusion'', la génération de particules, et quelques autres effets graphiques. Ils pourraient aussi être utilisés pour faire de la tesselation, mais leurs limitations font que ce n'est pas pratique. Rappelons que les ''geometry shaders'' sont optionnels et que beaucoup de jeux vidéos ou de moteurs de rendu 3D n'en utilisent pas. ===L'étape d’assemblage de primitives est dupliquée=== Les ''geometry shaders'' n'ont jamais eu de processeur de shader dédié, car ils ont été introduits avec DirectX 10 et OpenGl 3.2, en même temps que les processeurs de ''shaders'' ont étés unifiés (rendu capable d’exécuter n'importe quel ''shader''). Leur place dans le pipeline graphique est quelque peu étrange. En théorie, ils sont placés après l'assembleur de primitive, car ils manipulent les primitives fournies par l'étape d'assemblage des primitives. Mais le résultat fournit par les ''geometry shaders'' doit être retraité par l'assembleur de primitive. En effet, j'ai menti plus haut en disant que les ''geometry shaders'' fournissent en entrée de 0 à plusieurs primitives : la sortie d'un ''geometry shader'' est un ensemble de sommets, non-regroupés en primitives. Le résultat est que l'assembleur de primitive doit refaire son travail après le passage d'un ''geometry shader'', pour déterminer les primitives finales. Et il faut aussi refaire le ''culling'', au cas où les primitives générées ne soient pas visibles depuis la caméra. Heureusement, la sortie d'un ''geometry shader'' est soit un point, soit une ligne, soit un ''triangle strip'', ce qui simplifie la seconde phase d'assemblage des primitives. Avec les ''geometry shaders'', il y a donc deux phases d'assemblage des primitives : une phase avant, décrite dans la section précédente, et une seconde phase simplifiée après les ''geometry shaders''. Il n'y a pas que la phase d'assemblage de primitives qui est dupliquée : le tampon de primitives l'est aussi. On trouve donc un tampon de primitives à l'entrée des ''geometry shaders'' et un autre à la sortie. [[File:Implémentation matérielle des geometry shaders.png|centre|vignette|upright=2|Implémentation matérielle des geometry shaders]] L'implémentation des tampons de primitive est assez compliquée par la spécification des ''geometry shaders''. Un ''geometry shader'' fournit un résultat très variable en fonction de ses entrées. Pour une même entrée, la sortie peut aller d'une simple primitive à plusieurs dizaines. Le ''geometry shader'' précise cependant un nombre limite de sommets qu'il ne peut pas dépasser en sortie. Il peut ainsi préciser qu'il ne sortira pas plus de 16 sommets, par exemple. Mais ce nombre est généralement très élevé, bien plus que la moyenne réelle du résultat du ''geometry shader''. Or, le tampon de primitives de sortie a une taille finie qui doit être partagée entre plusieurs instances du ''geometry shader''. Et cette répartition n'est pas dynamique, mais statique : chaque instance reçoit une certaine portion du tampon de primitive, égale à la taille du tampon de primitives divisée par ce nombre limite. Aussi, le nombre d'instance exécutables en parallèle est rapidement limitée par le nombre de sommets maximal que peut sortir le ''geometry shader'', nombre qui est rarement atteint en pratique. ===La fonctionnalité de ''stream output''=== Une fonctionnalité des ''geometry shaders'' est la possibilité d'enregistrer leurs résultats en mémoire. Il s'agit de la fonctionnalité du '''''stream output'''''. On peut ainsi remplir une texture ou le ''vertex buffer'' dans la mémoire vidéo, avec le résultat d'un ''geometry shader''. Notons que celle-ci mémorise un ensemble de primitives, pas autre chose. Cette fonctionnalité est utilisée pour certains effets ou rendu bien précis, mais il faut avouer qu'elle n'est pas très souvent utilisée. Aussi, les concepteurs de cartes graphiques n'ont pas optimisé cette fonctionnalité au maximum. Le ''stream output'' n'a généralement pas accès prioritaire à la mémoire, comparé aux ROP, et n'a souvent accès qu'à une partie limitée de la bande passante mémoire. Notons qu'il existe deux formes de ''stream output'' : une qui permet aux ''vertex shader'' d'écrire dans une texture, l'autre qui permet aux ''geometry shaders'' de le faire. Notons que le ''stream output'' fournit un flux de primitives, pas de sommets, même pour le flux sortant d'un ''vertex shader''. En clair, beaucoup de sommets sont dupliqués et ont n'a pas d{{'}}''index buffer''. Les résultats du ''stream output'' sont donc assez lourds et prennent beaucoup de mémoire. [[File:Stream output.png|centre|vignette|upright=2.5|Stream output]] ==DirectX 12 : les ''mesh shaders''== [[File:D3D11 Pipeline.svg|vignette|upright=1|Pipeline graphique de Direct x 11.]] Avec l'introduction des ''geometry shaders'' et de la tesselation, le pipeline graphique est devenu très complexe. Plusieurs étages en plus sont ajoutés à sa portion géométrique : un pour les ''geometry shaders'', trois pour la tesselation, et ce en plus des ''vertex shaders'' existants et des étages non-programmables. Le pipeline en question est celui d'Open GL 4 et de DirectX 11. Mais Direct X 12 a simplifié le tout, sous l'impulsion de technologies introduites par AMD et de NVIDIA. AMD a introduit les ''primitive shaders'', NVIDIA a introduit les ''mesh shaders'''' ont été introduit par NVIDIA. Les derniers ont été gardés pour DirectX 12, simplifiant grandement le pipeline. ===Les primitive/mesh shaders=== Les deux solutions de AMD et NVIDIA partent du même principe : elles fusionnent certaines étapes du pipeline. Les ''primitive/mesh shaders'' font disparaitre les étapes d{{'}}''input assembly'' et d'assemblage de primitives, qui sont maintenant gérées par les ''primitive/mesh shaders''. Les ''primitive/mesh shaders'' lisent directement le tampon d'indice et lisent les sommets depuis la VRAM, sans passer par une étape non-programmable. Ils assemblent les primitives eux-mêmes et les envoient directement au rastériseur. Le tout permet des optimisations très intéressantes, comme un ''culling'' précoce. Les ''mesh shaders'' sont des ''shaders'' généralistes, semblables aux ''compute shaders''. Pour rappel, un ''compute shader'' peut lire des données en RAM, exécuter des traitements dessus, et enregistrer les résultats en RAM. Il peut lire ou écrire à des adresses arbitraires, sans limitations. Il n'est pas limité à lire des données consécutives, peut sauter d'une donnée à une autre donnée distante en RAM. Les ''mesh shaders'' sont des variantes des ''compute shaders'', qui n'écrivent pas leur résultat en RAM, mais envoient celui-ci au rastériseur. Plus précisément, ils écrivent leur résultat dans le tampon de primitives. Les ''mesh shaders'' peuvent contourner l'étape d{{'}}''input assembly'' et la remplacer par leur propre code. Pour rappel, l'étape d{{'}}''input assembly'' était non-programmable et gérait des tampons de vertices et d'indices très normés. Les sommets étaient lus soit un par un, soit par paquets de N sommets consécutifs, ce qui était assez rigide. Il n'y avait pas d'accès arbitraire en mémoire RAM comme peuvent le faire les ''compute shaders''. Par contre, un ''mesh shader'' peut accéder aux sommets de la manière qu'il souhaite, ce qui permet d'émuler un ''input assembler'' normal et plus encore. Une autre différence avec les ''vertex shaders'' est qu'ils ne traitent pas forcément des sommets, mais peuvent aussi envoyer des primitives au rastériseur directement. En clair, ils n'ont pas besoin d'une étape de ''primitive assembly'', qu'ils peuvent émuler directement dans le ''shader'' lui-même. Le ''culling'' est lui aussi réalisé par le ''primitive shader'', pas par une unité fixe. Et cela permet de contourner un problème fondamental des ''vertex shaders'' : il fallait que les primitives soient assemblées pour qu'on puisse déterminer si elles sont ou non invisibles. A l'opposé, les ''primitive/mesh shaders'' assemblent les primitives de manière précoce dans le ''primitive/mesh shader'', ce qui permet d'éliminer les primitives invisibles le plus tôt possible. Pour cela, les opérations permettant de déterminer si une primitive est visible sont exécutés en priorité, les autres opérations sont retardées et effectuées le plus tard possible. Ainsi, les calculs pour colorier ou orienter un sommet ne sont pas exécutés si le sommet est invisible. Il y a des différences entre ''primitive'' et ''mesh shaders''. Les ''primitive shaders'' permettent de lire un sommet à la fois, alors que les ''mesh shaders'' permettent de lire des ''batchs'' de plusieurs primitives d'un coup. Ces ''batchs'' de plusieurs primitives sont appelés des meshlets. La différence n'est pas fondamentale : le hardware des cartes AMD, qui gère des ''primitive shaders'', peut regrouper dynamiquement plusieurs instances de ''primitive shaders'' en un seul ''mesh shader'', via les technique de SIMT (une instance de ''primitive shader'' effectue des opérations scalaires, qui peuvent être regroupées en une seule instance SIMD en traitant plusieurs sommets en parallèle). La seule différence est que les ''mesh shaders'' exposent ce comportement au niveau du jeu d'instruction des ''shaders'', les programmeurs en ont conscience. ===Le pipeline géométrique avec les ''primitive/mesh shaders''=== Avec les ''primitive shaders'', l'implémentation exacte dépend de si la tesselation est activée ou non. Si la tesselation n'est pas activée, le ''vertex shader'' et le ''geométry shader'' sont fusionnés en un seul ''primitive shader''. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, sans tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="4" | |- ! DirectX 12 | colspan="4" | ''Primitive shader'' (AMD) |} Avec la tesselation activée, les ''geometry shaders'' et les ''domain shaders'' en un seul ''shader''. De même, les ''vertex shaders'' et les ''hull shaders'' sont fusionnés en un seul ''shader'', nommé l{{'}}''amplification shader''. Ainsi, le pipeline graphique est grandement simplifié, avec seulement deux ''shaders'' et un étage fixe, au lieu de quatre ''shaders'' différents. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, avec tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="7" | |- ! DirectX 12 | colspan="3" | * ''Amplification shader'' (AMD) | class="f_rouge" | Tesselation | colspan="3" | * ''Primitive shader'' (AMD) |} <noinclude> {{NavChapitre | book=Les cartes graphiques | prev=Le pipeline géométrique : évolution | prevText=Le pipeline géométrique : évolution | next=Le rasterizeur | nextText=Le rasterizeur }}{{autocat}} </noinclude> 3am2tzvqv107acqm9vsaggku8w3gcf4 Les cartes graphiques/La répartition du travail sur les unités de shaders 0 79263 763460 761800 2026-04-11T16:00:18Z Mewtow 31375 /* Sources extérieures */ 763460 wikitext text/x-wiki Un GPU plusieurs processeurs de shaders, chacun traitant plusieurs sommets/pixels à la fois. La répartition du travail sur plusieurs processeurs de ''shaders'' est un vrai défi sur les cartes graphiques actuelles. La répartition du travail sur plusieurs processeurs de ''shaders'' est le fait du '''processeur de commande''', un circuit de la carte graphique. Ce n'est pas son seul rôle, mais c'est clairement une fonctionnalité très importante que prend en charge le processeur de commande. ==La répartition du travail pour le GPGPU== Avant de voir ce qu'il en est pour le rendu 3D, nous allons faire un détour par les fonctionnalités dites de GPGPU. Outre le rendu 3D, les cartes graphiques modernes sont utilisées pour accélérer des calculs scientifiques, tout ce qui implique des réseaux de neurones, de l'imagerie médicale, etc. De manière générale, tout calcul faisant usage d'un grand nombre de calculs sur des matrices ou des vecteurs est concerné.. L'usage d'une carte graphique pour autre chose que le rendu 3D porte le nom de '''GPGPU''' (''General Processing GPU''). En soi, le GPGPU est assez logique : les processeurs de shaders, bien que conçus avec le rendu 3D en tête, n'en restent pas moins des processeurs multicœurs SIMD/VLIW assez puissants. Si nous voyons le GPGPU avant le rendu 3D, c'est pour une raison simple : la répartition du travail sur les processeurs de shaders est alors nettement plus simple. En GPGPU, les shaders font des calculs génériques, à savoir qu'ils ne travaillent pas sur des pixels ou des vertices. Ils n'ont donc pas à communiquer avec le rastériseur ou l'''input assembler'', ils ne lisent même pas de textures dans la RAM. Les processeurs de shaders communiquent seulement avec la mémoire vidéo, mais pas avec le moindre circuit fixe. La répartition du travail en GPGPU est donc beaucoup plus simple qu'en mode graphique, le processeur de commande a nettement moins de travail. ===La répartition du travail en GPGPU n'est pas celle du mode graphique=== Du point de vue du GPGPU, l'architecture d'une carte graphique récente est illustrée ci-dessous. Les processeurs/cœurs sont les rectangles en bleu/rouge, le bleu et le rouge correspondant à des circuits de calcul différents. La hiérarchie mémoire est indiquée en vert. Le tout est alimenté par un processeur de commande, en jaune, ici appelé le ''Thread Execution Control Unit''. [[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, ayant chacun plusieurs unités de calcul appelées malencontreusement "processeurs de threads". 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.]] En GPGPU, un ''shader'' s'exécute sur des regroupements de données bien connus des programmeurs : des tableaux. Pour rappel, un tableau est un ensemble d'entiers ou de flottants qui sont consécutifs en mémoire RAM. Les tableaux peuvent être de simples tableaux, des matrices, peu importe. Le processeur envoie à la carte graphique un ''shader'' à exécuter et les tableaux à manipuler. Les tableaux ont une taille variable, mais sont presque toujours de très grande taille, au moins un millier d’éléments, parfois un bon million, si ce n'est plus. Le processus de répartition du travail est globalement le suivant. Le processeur de commande reçoit une '''commande de calcul GPGPU''', qui précise quel ''shader'' exécuter, et fournit l'adresse de plusieurs tableaux, ainsi que des informations sur le format des données (entières, flottantes, tableau en une ou deux dimensions, autres). Le processeur de commande découpe les tableaux en vecteurs de taille fixe, qu'un processeur de shader peut gérer. Par exemple, si un processeur SIMD gère des vecteurs de 32 entiers/flottants, alors le tableau est découpé en morceaux de 32 entiers/flottants, et chaque processeur exécute une instance du ''shader'' sur des morceaux de cette taille. Cependant, il faut tenir compte que les processeurs de shader sont multithréadés. Ils peuvent gérer plusieurs ''threads'', qui sont exécutés selon les besoins. Si un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' prend la relève. Un processeur de shader permet d'exécuter "en même temps" entre 8 et 64 ''threads''. La conséquence que l'on peut envoyer plusieurs morceaux de tableau sur un processeur de shader. Chaque morceau de tableau est combiné avec un shader pour former un ''thread'', et l'on envoie 8 à 64 de ces ''threads'' sur un même processeur de shader. Pour résumer, on découpe le travail en morceaux de taille identique, qu'on envoie à chaque processeur de shader. Un GPU moderne est une sorte de processeur multicœurs amélioré, qui gère des tableaux/vecteurs de taille variable, mais les découpe en vecteurs de taille fixe à l'exécution et répartit le tout sur des processeurs SIMD. : Il faut préciser que la terminologie du GPGPU est quelque peu trompeuse. Dans la terminologie GPGPU, un ''thread'' correspond à l'exécution d'un ''shader'' sur une seule donnée scalaire, un seul entier/flottant provenant du tableau. Beaucoup de monde s'imagine que les processeurs de ''shader'' exécutent des ''threads'', qui sont regroupés à l'exécution en ''warps'' par le matériel, mais ce n'est certainement pas ce qui se passe. ===La répartition du travail telle que définie par CUDA=== Pour les GPU NVIDIA, le processus de découpage n'est pas très bien connu. Mais on peut en avoir une idée en regardant l'interface logicielle utilisée pour le GPGPU. Chez NVIDIA, celle-ci s'appelle CUDA et ses versions donnent une idée de comment le découpage s'effectue. Premièrement, les ''shaders'' sont appelés des '''''kernels'''''. Les tableaux de taille variable sont appelés des '''''grids'''''. Les données individuelles sont appelées, de manière extrêmement trompeuse, des ''threads''. Aussi, pour éviter toute confusion, je vais renommer les ''threads CUDA'' en '''scalaires'''. Les ''grids'' sont eux-même découpés en '''''thread blocks''''', qui contiennent entre 512 et 1024 données entières ou flottantes, 512 et 1024 scalaires. La taille, 512 ou 1024, dépend de la version de CUDA utilisée, elle-même liée au modèle de carte graphique utilisé. Plutôt que d'utiliser le terme ''thread blocks'', je vais parler de '''bloc de scalaires'''. Le bloc de scalaire est une portion d'un tableau, un bloc de mémoire, une suite d'adresse consécutives. Il a donc une adresse de départ. Chaque scalaire d'un bloc de scalaire a un indice qui permet de déterminer sa position dans le tableau, qui est calculé par le processeur de commande. Le calcul de l'indice peut se faire de différentes manières, suivant que le tableau soit un tableau unidimensionnel (une suite de nombre) ou bidimensionnel (une matrice). CUDA gère les deux cas et les dernières cartes graphiques gèrent aussi des tableaux à trois dimensions. Le calcul de l'adresse d'un scalaire se fait en prenant l'adresse de départ du bloc de scalaire, et la combinant avec les indices. [[File:Block-thread.svg|centre|vignette|upright=2|Découpage des tableaux avec CUDA.]] Les processeurs de ''shader'' sont appelés des '''''streaming multiprocessor''''', terme encore une fois trompeur. Une fois lancé sur un processeur de ''shader'', le shader lit un bloc de scalaire et l'utilise pour faire ses calculs. Il y reste définitivement : il ne peut pas migrer sur un autre processeur en cours d'exécution. Un processeur de ''shader'' peut exécuter plusieurs instances de ''shaders'' travaillant sur des bloc de scalaires différents. Dans le meilleur des cas, il peut traiter en parallèle environ 8 à 16 bloc de scalaires différents en même temps. [[File:Software-Perspective for thread block.jpg|centre|vignette|upright=2|Exécution des ''thread blocks'' sur les processeurs de ''shaders''.]] Le bloc de scalaires est découpé en vecteurs d'environ 32 entiers/flottants, appelés des '''''warps''''' dans la terminologie NVIDIA/CUDA, des '''''wavefronts''''' dans la terminologie AMD. Avec 512/1024 éléments par bloc de scalaire, découpé en ''warps'' de 32, cela donne 16 à 32 ''warps'' suivant la version de CUDA utilisée. Le découpage en ''warps'' est encore une fois le fait du processeur de commande. Le terme ''warp'' est aussi utilisé pour décrire l'instance du ''shader'' qui fait des calculs avec un ''warp''. Un ''warp''/''wavefront'' est donc en réalité un ''thread'', un programme, une instance de ''shader'', qui manipule des vecteurs SIMD de 32 éléments. Les 16 à 32 ''warps'' sont exécutés en même temps sur le processeur de ''shader'', via ''multithreading'' matériel, à savoir que le processeur les exécute à tour de rôle. Un ''warp'' exécute des instructions SIMD sur des vecteurs de 32 éléments, donc de taille fixe. Pour résumer, plus un GPU contient de processeurs de ''shaders'', plus le nombre de blocs de scalaires qu'il peut traiter en même temps est important. Par contre, la taille des ''warps'' ne change pas trop et reste la même d'une génération sur l'autre. Cela ne signifie pas que la taille des vecteurs reste la même, mais elle est assez contrainte. ===La répartition avec une file de ''threads''=== Pour résumer, les tableaux sont découpés en morceaux et chaque morceau est combiné avec un shader pour former un ''thread''. reste à répartir ces ''threads'' sur les processeurs de shaders. La répartition la plus simple est la suivante : le premier morceau va dans le premier processeur de shader, le second morceau va dans le second processeur de shader, etc. En clair, un simple algorithme du tourniquet. Si elle été utilisée sur les anciens GPU, notamment sur les cartes graphiques SGI des années 80-90, ce n'est pas celle qui est utilisée aujourd'hui. A la place, la répartition demande une coopération entre le processeur de commande et les processeurs de shaders. Les GPU modernes incorporent une '''file de ''threads''''', entre le processeur de commande et les processeurs de shaders, qui sert de file d'attente. Le processeur de commande découpe les tableaux en ''threads'', qui sont ajoutés dans cette file d'attente. Les processeurs de shader récupèrent ensuite les ''threads'' disponibles dans cette file d'attente. Le processeur de commande remplit la file de ''thread'' tant que celle-ci n'est pas pleine. Et la file de ''thread'' se vide quand un processeur de shader est libre, qu'il a finit de calculer le ''thread'' précédent. La répartition est alors dynamique et exploite au mieux les processeurs de shader. Prenons l'exemple d'un GPU avec une file de ''thread'' capable de mémoriser 32 ''threads'', et 16 processeurs de shader. Soumettons un tableau de 24 éléments. Le processeur de commande remplit la file de ''thread'' avec 16 ''threads''. Les processeurs de shader lisent alors chacun un ''thread'', ce qui fait que la file d'attente se vide de 16 ''threads'', il n'en reste que 8. Une fois qu'un shader a finit son travail, il lit un ''thread'' dans la file d'attente, ce qui fait qu'elle se vide un ''thread'' après l'autre. ===L'exécution simultanée des commandes=== Maintenant, regardons ce qui se passe quand on envoie deux commandes successives. Prenons deux commandes de 24 ''threads'' chacune, avec 16 processeurs de shaders. Pour simplifier, les processeurs de shader vont d'abord exécuter les 16 ''threads'' de la première commande, et on va supposer qu'ils vont tous se terminer quasiment en même temps. Le processeur de commande remplit alors la file de commande : en plus des 8 ''threads'' restants, il rajoute 8 ''threads'' provenant de la commande suivante. Les processeurs de shader exécutent alors les 16 commandes dans la file : la moitié vient de la première commande, l'autre vient de la seconde commande. En clair, la file de ''thread'' permet une forme de "pipeline", un terme connu de ceux qui ont déjà lu un cours d'architecture des ordinateurs. L'idée est que l'on peut lancer une nouvelle commande alors que la précédente n'est pas terminée. Il s'agit de l'idée générale, mais les détails peuvent être assez surprenants. Par exemple, si on veut exécuter pleins de commandes très petites, elles peuvent s'exécuter en parallèle dans des processeurs de shader séparés. Par exemple, avec 32 processeurs de shader, vous pouvez exécuter 16 commandes de un ''thread'' chacune. Ou encore, une commande lancée récemment peut se terminer avant une commande plus ancienne. Bref : la file de ''thread'' permet de lancer plusieurs commandes l'une après l'autre, mais elles peuvent s’exécuter en même temps et se terminer dans le désordre. Tout fonctionne à la perfection tant que les commandes de calcul sont indépendantes. Mais il arrive qu'une commande prenne en entrée le résultat d'une commande précédente. Dans ce cas, lancer les deux commandes en même temps peut poser problème. La seconde commande peut alors tenter de lire un résultat qui n'a pas encore été calculé. Et il ne s'agit là que d'un cas particulier, mais de nombreuses dépendances assez complexes entre commandes existent. De telles dépendances imposent que la première soit intégralement terminée avant que la seconde démarre. On parle alors de ''partial pipeline flush''. Pour gérer cela, le processeur de commande supporte des '''commandes de synchronisation'''. Les plus simples, les commandes ''FLUSH'' empêchent le démarrage d'une commande tant que les précédentes sont en cours. Elles empêchent le processeur de commande d'ajouter des ''threads'' dans la file de ''thread'', du moins tant que celle-ci n'est pas entièrement vide, et aussi tant que les processeurs de shader sont occupés. Ces commandes de synchronisation sont aussi appelées des '''barrières GPU'''. le terme indique bien qu'elle séparent le flux de commandes en deux. Les barrières GPU ont un cout en performance, car elles empêchent d'exécuter plusieurs commandes en même temps. Mais elles sont nécessaires. Par exemple, reprenons l'exemple de deux commandes de 24 ''threads'' chacune, séparées par une barrière GPU, toujours avec 16 processeurs de shader. Le GPU va d'abord exécuter les 16 premiers ''threads''. Puis, il va exécuter les 8 ''threads'' restants de la première commande, mais pas plus. La barrière GPU l'empeche d'exécuter les ''threads'' de la commande suivante. C'est seulement ensuite qu'il lancera les 16 ''threads'' de la seconde commande, puis les 8 restants. Cela a pris plus de temps que d'exécuter les deux commandes en même temps. D'autres barrières GPU sont plus précises. Elles empêchent le démarrage d'une nouvelle commande tant que la commande précédente n'a pas atteint un certain stade de son exécution. Elles permettent de gagner en performance en démarrant la commande suivante au plus tôt. De telles commandes sont des commandes du style "attend que le registre de statut numéro N contienne la valeur adéquate avant de démarrer la commande suivante". Une alternative réserve une adresse mémoire, dans laquelle la commande précédente écrit une valeur prédéterminée pour dire qu'elle a finit. ==La répartition du travail pour le rendu graphique== Après avoir vu le cas du GPGPU, nous allons voir le cas du rendu 3D. La différence est que les commandes GPGPU sont remplacées par des commandes 3D, qui demande d'afficher un objet. Du point de vue de l'API 3D, elles correspondent grossièrement soit à un ''draw call'', soit à un changement de ''render state''. Aussi, nous ferrons parfois la confusion entre les deux, bien que ce soit techniquement une grosse simplification, plus proche de l'erreur ou de la confusion que de l'abus de langage. Les commandes graphiques sont différentes des commandes de calcul, mais elles fonctionnent sur le même principe. La différence est qu'une commande graphique demande d'afficher un objet 3D, les ''draw call'' affichant une image objet par objet. Une commande graphique contient bien un tableau de données : un tableau de triangle qui correspond à l'objet à afficher. Cependant, elle contient aussi une liste de texture et une liste de shaders. Le tableau de triangles est découpé en ''threads'' qui sont envoyés aux processeurs de shaders. Il est possible d'imaginer un GPU qui attend qu'une commande soit terminée avant de démarrer la suivante. Il s'agit d'une technique simple, mais tellement peu performante qu'il n'est pas certain qu'elle ait été utilisée en pratique. En pratique, tous les GPUs exécutent plusieurs commandes graphiques consécutives en même temps, comme pour les commandes de calcul. Par exemple, si une commande simple n'utilise que 3 processeurs de shaders sur 8, la commande suivante peut être lancée pour occuper les 5 processeurs de shader restants. Un point important est que ce n'est possible que si les circuits fixes ne sont pas un facteur limitant. Si la première commande sature les circuits fixe, le lancement de la seconde commande sera empêché. Concrètement, du point de vue de l'API graphique, le GPU peut exécuter plusieurs ''draw call'' en même temps. Pire que ça, un ''draw call'' lancé après un autre peut finir avant ! Les ''draw call'' sont donc traités dans un désordre tout relatif, mais loin de ressembler à un ordre strict. Et la conséquence, c'est que les problèmes de dépendances vus plus haut reviennent. Par exemple, imaginez qu'un jeu écrive une ''shadowmap'' dans une texture, puis l'utilise dans un algorithme d'éclairage. Une première commande calcule et écrit la ''shadowmap'', une seconde commande exécute l'algorithme d'éclairage. Il est interdit de démarrer la seconde commande tant que la première commande n'a pas calculé son résultat, la ''shadowmap'' n'est pas encore prête. Et une barrière GPU est alors nécessaire. Elle est ajoutée par le ''driver'', ou par le programmeur s'il utilise une API 3D récente (DirectX 12, Vulkan). Un autre exemple survient quand deux ''draw calls'' consécutifs utilisent des ''render state'' différents. Dans ce cas, le GPU voit deux commandes de rendu, avec une commande de changement d'état entre les deux. La commande de changement d'état fait alors deux choses : le changement d'état, mais aussi une barrière GPU. Concrètement, le processeur de commande ne démarre le second ''draw call'' que quand le changement d'état est terminé. Tout cela est géré par le processeur de commande, qui détermine dans quel ordre lancer les commandes, quand émettre des barrières, etc. On retrouve aussi des files de ''threads'', ou du moins leur équivalent pour le rendu graphique. Pour le moment, on ne voit pas de différence avec le calcul GPGPU : le processeur de commande et la file de ''thread'' sont là, elles sont exploitées de manière à exécuter des commandes graphiques en parallèle sur des processeurs de shaders, etc. Une grosse différence est qu'il y a plusieurs files de ''threads'', dont le nombre exact dépend du GPU considéré. Une autre différence est la présence de circuits fixes, à savoir un rastériseur, un ''input assembler'' et des ROPs. Voyons cela en détail. ===Les GPU avec une seule unité géométrique=== Avant de poursuivre, faisons une première remarque : un triangle est affiché sur un ou plusieurs pixels lors de l'étape de rastérisation. Un triangle peut donner quelques pixels lors de l'étape de rastérisation, alors qu'un autre va couvrir 10 fois de pixels, un autre seulement trois fois plus, un autre seulement un pixel, etc. La conséquence est qu'il y a plus de travail à faire sur les pixels que sur les sommets. C'est un phénomène d''''amplification''', qui explique qu'il y a plus de processeurs pour les ''pixel shaders'' que pour les ''vertex shaders''. Et il impact grandement la répartition du travail sur les processeurs de shaders. Le cas le plus simple est celui des GPU avec une unité géométrique, qui alimente un rastériseur, qui lui-même alimente plusieurs unités de pixel/texture. L'''input assembler'' envoie des sommets à l'unité géométrique, dès que celle-ci est inoccupée, prête à accepter une nouvelle tâche à faire. De son côté, le rastériseur distribue les pixels aux unités de pixel/texture. Les GPU de ce genre sont assez rares, et surtout assez anciens. Les premières cartes graphique de l'entreprise SGI était de ce type, la Geforce 256 de NVIDIA l'était aussi. [[File:Architecture d'un GPU tenant compte de l'amplification des pixels.png|centre|vignette|upright=2.5|Architecture d'un GPU tenant compte de l'amplification des pixels]] La seule difficulté est de gérer l'amplification des pixels, la répartition des pixels sur les unités de pixel, et c'est le rastériseur qui s'en charge. Pour cela, le rastériseur utilise une '''file de pixels''', similaire à celle utilisé pour les commandes GPGPU, à la différence qu'elle mémorise des pixels à texturer/éclairer, sans compter qu'elle est placée plus loin dans le pipeline. Le rastériseur accumule les pixels qu'il génère dans cette file de pixel, les unités de pixel piochent le travail à faire dedans. La file de pixels est techniquement une mémoire FIFO dite multiport, à savoir qu'on peut lire plusieurs pixels en une seule fois. Pour être précis, la file de pixel est gérée par un circuit de répartition (''dispatcher''), qui répartit les pixels sur les unités de pixel libres. Le circuit de répartition peut regrouper les pixels générés en paquets de 43 à 64 pixels, pour tenir compte du caractère SIMD des processeurs de shaders. Les paquets générés sont techniquement des ''threads'', similaire aux ''threads'' GPGPU, sauf qu'ils ne contiennent que des pixels, avec tout ce qu'il faut pour les texturer ou les éclairer. [[File:Dispatch des shaders sur plusieurs processeurs de shaders.png|centre|vignette|upright=2|Dispatch des shaders sur plusieurs processeurs de shaders]] Il arrive que l'unité géométrique doive attendre que le rastériseur soit disponible. Par exemple, imaginons que le rastériseur traite un gros triangle, qui occupe une centaine de pixels à l'écran. Supposons que le GPU a 4 processeurs de shaders basiques, ce qui fait qu'il traite les 100 pixels générés à la rastérisation par paquets de 4. Traiter les 100 pixels prend 25 passes en tout. Pendant ce temps, l'unité géométrique a tout le temps pour calculer plusieurs triangles. Et gérer la situation peut se faire de deux manières. La première est la moins performante : l'unité géométrique est bloquée quand le rastériseur est occupé sur de gros triangles. Elle ne peut pas recevoir de nouveaux sommets à traiter, l'''input assembler'' est lui aussi en pause. Une solution alternative met en attente les triangles générés par l'unité géométrique, dans une mémoire FIFO dédiée. Ainsi, l'unité géométrique peut accumuler des triangles dans la mémoire FIFO, préparer du travail en avance pour le rastériseur. Le rastériseur consulte la FIFO quand il est libre. La FIFO se remplit tant que l'unité de rastérisation est occupée sur des gros triangles, puis elle est vidée progressivement quand elle traite des triangles plus petits. La mémoire FIFO en question est appelée la '''file de sommets transformés'''. : Notons que si la FIFO est pleine, on n'a pas le choix : l'unité géométrique est bloquée, elle n'accepte plus de nouveau triangles. [[File:GPU basique avec FIFO avant le rastériseur.png|centre|vignette|upright=2.5|GPU basique avec FIFO avant le rastériseur]] La présence d'une FIFO permet aussi d'implémenter facilement l'assemblage de primitives. Pour rappel, les unités géométriques calculent des sommets, alors que le rastériseur prend en entrée des triangles. Les sommets doivent donc être regroupés en triangles, lors d'une étape d'assemblage des primitives, qui précède la rastérisation. Il est possible d'ajouter une seconde mémoire FIFO entre l'assemblage de primitive et les rastériseur. La FIFO en question est appelée le '''tampon de primitive''', ou encore la file de primitives. [[File:GPU basique avec FIFO avant le rastériseur - prise en compte de l'assemblage de primitives.png|centre|vignette|upright=2.5|GPU basique avec FIFO avant le rastériseur - prise en compte de l'assemblage de primitives]] Pour résumer, l’implémentation demande deux choses : * l'ajout d'une file de sommets transformés entre l'unité géométrique et le rastériseur ; * l'ajout d'une file de pixels et d'une unité de distribution en sortie du rastériseur. Ces deux modifications seront conservées dans les GPU ultérieurs, avec quelques modifications mineures. Il faut dire qu'ajouter des mémoires FIFOs a des avantages certains. La présence des mémoires FIFO désynchronise deux étapes consécutives du pipeline, tout en gardant une exécution des étapes dans l'ordre. Une étape écrit ses résultats dans la mémoire FIFO, l'étape suivante lit ce tampon quand elle démarre de nouveaux calculs, la première étape n'a pas à attendre que la seconde soit disponible pour lui envoyer des données. Sauf si la mémoire FIFO est pleine, car elle ne peut plus accepter de nouveaux sommets/pixels, évidemment. ===Les GPU avec des processeurs séparés pour les ''vertex'' et ''pixel shaders''=== Maintenant, regardons ce qui se passe si l'on ajoute plusieurs unités géométriques. Les GPU de ce type commencent avec la Geforce 2 de NVIDIA et se terminent tout de même avec la Geforce 6/7. Soit une bonne décennie de GPU de ce type. De tels GPUs sont plus simples pour plusieurs raisons, la principale étant qu'il n'ont qu'un seul circuit rastériseur. La répartition du travail avec plusieurs rastériseurs est en effet beaucoup plus compliquée qu'avec un seul. [[File:Parallélisme dans une carte 3D.png|centre|vignette|upright=3|Parallélisme dans une carte 3D]] Notons que cette séparation marche ausis pour les GPU qui ont des processeurs de shaders. De tels GPUs ont des processeurs séparés pour les ''vertex shaders'' et les ''pixel shaders''. La raison est que DirectX 9.0 et OpenGL avaient des jeux d'instruction différents pour les ''vertex'' et ''pixel shaders''. Par exemple, les ''pixels shaders'' de l'époque pouvaient accéder aux textures, pas les ''vertex shaders''. Les GPU de ce type ont beaucoup plus de processeurs de ''pixel shaders'' que de processeurs de ''vertex shaders'', en raison du phénomène d'amplification de pixels mentionné plus haut. Pour donner un exemple, la Geforce 6800 avait 16 processeurs pour les ''pixel shaders'' et 6 processeurs pour les ''vertex shaders''. Un problème de ces GPU est que la répartition entre puissance entre ''vertex'' et ''pixel shaders'' est fixe. Mais tous les jeux vidéos n'ont pas les mêmes besoins : certains sont plus lourds au niveau géométrie que d'autres, certains ont des ''pixels shaders'' très gourmands avec des ''vertex shaders'' très ''light'', d'autres font l'inverse, etc. La répartition idéale est variable d'un jeu vidéo à l'autre, voire d'un niveau de JV à l'autre, et ces GPU en étaient loin. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=1.5|Carte 3D avec pixels et vertex shaders non-unifiés.]] Ceci étant dit, voyons comment la répartition du travail se fait sur de tels GPUs. La présence de plusieurs unités géométriques a deux conséquences : il faut alimenter plusieurs unités géométriques en triangles/sommets, il faut gérer l'envoi des triangles au rastériseur. Les deux demandent des solutions distinctes. La répartition sur les processeurs de ''vertex shader'' utilise encore une fois une file dédiée, sur le même modèle que la file de ''threads'' GPGPU. La file en question est appelée la '''file de sommets non-transformés''', pour la distinguer de celle située avant l'étape de rastérisation. Les deux mémorisent des sommets, mais la première se situe avant le ''vertex shader'', l'autre après. La gestion de cette file de sommets non-transformés est le fait de l'''input assembler''. Pour remplir la file, l'''input assembler'' lit le tampon de sommets en mémoire vidéo, ainsi que le tampon d'indice. Il crée alors des paquets de sommets, qui sont envoyés aux processeurs de shaders (remplacer par les unités de T&L sur les anciens GPU). Typiquement, il utilise des paquets de 32/64 sommets. Le fait de regrouper les sommets en paquets permet de profiter de la nature SIMD des processeurs de shaders, à savoir qu'un paquet est traité en bloc par une instance de shader. Voyons maintenant ce qui se passe après les ''vertex shaders''. Les unités géométriques envoient des sommets à un rastériseur unique, qui est donc un point de convergence, où plusieurs unités géométriques envoient leurs résultats. Pour gérer cette convergence, les GPU modifient la file de sommets transformés, celle située juste avant le rastériseur. L'idée est qu'elle est connectée à tous les processeurs de ''vertex shaders'', et qu'elle peut recevoir plusieurs triangles à la fois. Rien de compliqué à cela, ce n'est pas si compliqué de créer des mémoires RAM usuelle capables de supporter plusieurs écritures simultanées. Il suffit d'ajouter des ports d'écritures à la mémoire FIFO et le tour est joué. Pour résumer, l'implémentation demande d'ajouter une file de ''thread'' en amont de l'''input assembler'', et de modifier la mémoire FIFO en amont du rastériseur. La FIFO devient une mémoire multiport et est connectée à tous les processeurs de ''vertex shader''. Encore une fois, ces deux détails vont se retrouver dans les GPU qui vont suivre, avec quelques modifications mineures. ===Les GPU avec des shaders unifiés=== Il est maintenant temps de passer aux processeurs avec des processeurs de shaders unifiés. Depuis DirectX 10, le jeu d'instruction des ''vertex shaders'' et des ''pixels shaders'' a été unifié : il n'y a plus de différences entre les deux. En conséquence, il n'y a plus de distinction entre processeurs de ''vertex shaders'' et de ''pixels shaders'', chaque processeur pouvant traiter indifféremment l'un ou l'autre. L'usage de '''''shaders'' unifiés''' permet d'adapter la répartition entre ''vertex shaders'' et ''pixels shaders'' suivant les besoins de l'application, là où la séparation entre unités de vertex et de pixel ne le permettait pas. [[File:Architecture de base d'une carte 3D - 6.png|centre|vignette|upright=1.5|Carte 3D avec ''pixels'' et ''vertex shaders'' unfifiés.]] Une implémentation simple utilise toujours un rastériseur unique et un ''input assembler''. On retrouve encore une fois une file de pixel, une file de sommets transformées et une file de sommets non-transformés. Et le tout est connecté aux processeurs de shaders. Le rastériseur est connecté en entrée comme en sortie sur tous les processeurs de shaders, l’''input assembler'' envoie des triangles à tous les processeurs de shaders, etc. Pour cela, les deux files de sommets sont connectées à tous les processeurs de ''shader'', et il en est de même pour la file de pixels. Les processeurs de shaders lisent dans ces files pour récupérer du travail à faire. Un premier problème est qu'il faut éviter qu'ils se marchent sur les pieds. Par se marcher sur les pieds, on veut dire que le rastériseur et l’''input assembler'' ne doivent pas envoyer du travail à un même processeur de shader en même temps. Si l'''input assembler'' démarre un ''vertex shader'' sur un processeur de shader, le rastériseur ne peut pas démarrer un ''pixel shader'' dessus. Une solution serait de privilégier la file de pixel sur la files de sommets non-transformés. Les processeurs de shader exécutent donc des pixels shaders en priorité sur les ''vertex shaders''. Un second problème est que l'implémentation demande beaucoup d'interconnexions. Elle marche encore quand on a peu de processeurs de shader, moins d'une trentaine. Mais au-delà, connecter un rastériseur à 30 processeurs de shader devient un véritable défi technique. Les interconnexions sont complexes à câbler, déplacer des données dedans demande beaucoup de courant, ça chauffe, la longueur des fils rend les transferts de données assez lents, la fréquence du GPU en souffre. La seule solution est d'utiliser plusieurs rastériseurs, chacun connecté à un nombre limité de processeurs de shaders. Maintenant, imaginez que le GPU incorpore plusieurs rastériseurs, afin de rastériser plus de triangles et d'améliorer les performances. Le câblage serait encore plus abominable avec des processeurs de shaders unifiés. Du moins, ce serait le cas si on souhaite connecter chaque rastériseur à tous les processeurs de shaders. Mais en réalité, il y a moyen d'utiliser plusieurs rastériseurs intelligemment. L'idée est que chaque rastériseur est connecté à un petit nombre de processeurs de shaders, pas à tous les processeurs. Par exemple, avec 35 processeurs de shaders, on peut avoir 7 rastériseurs, chacun connecté à 5 processeurs de shaders. Les interconnexions sont alors grandement simplifiées. ===Le cas des ROPs=== Nous venons de voir comment le GPU répartit le travail pour ce qui est de la rastérisation et des processeurs de shaders. Mais nous n'avons pas parlé des ROPs. Les ROPs sont techniquement des circuits fixes, qui s'occupent de la gestion du tampon de profondeur, mais aussi de certaines fonctionnalités comme l'''alpha blending''. Un GPU contient plusieurs ROPs, à l'exception de quelques GPU grand public des années 90. Et il faut connecter ces ROPs aux processeurs de shaders. Et là encore, il y a une différence entre les GPU avec des processeurs shaders unifiés et les autres. Avec des processeurs séparés pour les ''vertex shaders'' et les ''pixel shaders'', les ROPs sont connectés aux processeurs de pixels shaders. Pour cela, il y a deux possibilités. Avec la première, chaque ROP est connecté à une unité de pixel, rien d'autre. Elle a été utilisée sur de vielles cartes graphiques, dans les années 90 et avant. La seconde solution utilise des interconnexions complètes, à savoir que chaque ROP est connecté à toutes les unités de pixel. La Geforce 6800 utilisait cette solution, comme le montre le schéma précédent. Le ''fragment crossbar'' entre les processeurs de ''pixel shader'' et les ROPs est un réseau d'interconnexion, qui connecte les 16 processeurs de ''pixel shader'' aux 16 ROPs. [[File:GeForce 6800.png|centre|vignette|upright=3|GeForce 6800]] Avec les shaders unifiés, il faut connecter tous les ROPS à tous les processeurs de shaders. Le réseau d'interconnexion est juste plus complexe, car on ne connecte plus les ROPs seulement aux processeurs de pixel shader, mais à tous les processeurs de shaders. Le nombre d'interconnexion augmente donc. Et de nos jours, le nombre de ROPs et de processeurs de shaders est trop élevé pour que cette solution soit valable. La solution actuellement retenue est la même qu'avec les rastériseurs : on ne relie un ROP qu'à un nombre limité de processeurs de shaders. Et les interconnexions retenues sont les mêmes que celles utilisés pour le rastériseur. Un GPU moderne est organisé en plusieurs ''Graphic Processing Units'' (terminologie NVIDIA), que nous abrégerons en GPC. Ils regroupent chacun : plusieurs processeurs de shaders, un rastériseur et un ROP. Les processeurs de shaders envoient leurs résultats au ROP et au rasteriseur dans le GPC, pas à ceux dans un autre GPC. [[File:Graphic card architecture with unified shaders.png|centre|vignette|upright=2.5|Graphic card architecture with unified shaders]] Le fait de regrouper ainsi un rastériseur avec un ROP s'explique par le fait que les deux sont reliés entre eux. En effet, l'élimination des pixels cachés peut se faire de manière précoce, à savoir juste après la rastérisation. Les deux circuits s'enchainent alors l'un à la suite, ce qui fait qu'il vaut mieux vaut les relier ensemble. ==L'exécution de commandes différentes : graphiques et GPGPU== Les GPU modernes disposent de plusieurs processeurs de commandes, et donc de plusieurs files de commande. Et les chiffres peuvent monter assez haut. Par exemple, les GPU AMD utilisent un processeur de commande graphique, accompagné de plusieurs processeurs de commande dédiés au GPGPU. Les processeurs de commande spécifique au GPGPU étaient appelés des ACE, et il y en avait 8, 16 ou 32 selon l'architecture, le nombre ayant augmenté au cours du temps. Et chacun gérait plusieurs files de commandes séparées ! Par exemple, les GPU AMD d'architecture GCN ont 8 processeurs de commande GPGPU pour 64 files de commandes GPGPU, auxquels il faut ajouter le processeur pour les commandes graphiques en plus ! Les GPU NVIDIA d'architecture Pascal disposent eux de 32 files de commande matérielles, mais dont 31 sont réservées aux GPGPU. [[File:GCN command processing.svg|centre|vignette|upright=2|GCN command processing]] Mais quelle est l'intérêt ? Pour le dire vite : remplir des processeurs de shader inutilisés. Il arrive que des processeurs de shaders soient inutilisés, soit parce que la commande en cours d'exécution n'en a pas besoin. Il est théoriquement possible de les remplir avec des ''threads'' provenant de la commande suivante. Cependant, ce n'est pas toujours possible. Par exemple, il se peut que la commande suivante soit une barrière GPU, qui bloque l'avancée des commandes suivantes. Ou encore, que la file de commande soit vide, car les commandes suivantes ne sont pas encore arrivées. Une solution serait alors de chercher des commandes ailleurs, et de préférence des commandes capables de remplir des processeurs de shader. Mais où les trouver ? ===L'exécution simultanée de plusieurs applications sur le GPU=== La solution la plus simple est assez évidente, quand on se rappelle du chapitre sur les API 3D, et notamment de la section de fin sur le partage du GPU entre plusieurs applications. Un GPU est aujourd'hui sollicité par plusieurs logiciels en même temps : il est possible de lancer un jeu vidéo en fenêtré, pendant que OBS ou un logiciel de ''Streaming'' capture l'écran, avec Firefox et Discord et un programme de ''cloud computing'' de type ''Folding@Home'' qui tournent en arrière-plan. Et toutes ces applications sont accélérées par le GPU. Des situations de ce genre, où on doit partager le GPU entre plusieurs applications, sont assez courantes. Et c'est soit le pilote qui s'en charge, soit le GPU lui-même. * L''''arbitrage logiciel''' est la méthode la plus simple : tout est fait en logiciel. Concrètement, le système d'exploitation et/ou le pilote de la carte graphique se chargent du partage du GPU. Dans les deux cas, tout est fait en logiciel. * L''''arbitrage matériel''' délègue ce partage au GPU. Du moins en partie, car le système d'exploitation détermine quelle application a la priorité. L'avantage est que cela décharge le processeur d'une tâche assez lourde en calcul, pour la déporter sur le GPU. Sans compter que les mécanismes matériels d'arbitrage sont plus efficaces. Sur Windows, avant l'arrivée du modèle de driver dit ''Windows Display Driver Model'', il n'y avait pas d'arbitrage entre les applications. Il y avait une file de commande unique et les commandes étaient exécutées en mode "premier entré, premier sorti". Et ce n'était pas un problème car les seules applications qui utilisaient le GPU étaient des jeux vidéos fonctionnant en mode plein écran. Avec le modèle de driver ''Windows Display Driver Model'' (WDDM), l'arbitrage logiciel est apparu. Depuis 2020, avec l'arrivée de la ''Windows 10 May 2020 update'', Windows supporte l'arbitrage matériel. L'arbitrage matériel est implémenté grâce à la présence de plusieurs processeurs de commandes. L'idée est d'attribuer des priorités à chaque processeurs de commande. Et plus un processeur de commande est prioritaire, plus le GPU lui réserve de processeurs de shaders. Prenons l'exemple avec deux processeurs de commande, et donc deux files de ''thread'', et 16 processeurs de shaders. Si la première file de ''thread'' a la priorité, le GPU lui réserve 14 processeurs de shaders sur 16, contre seulement 2 pour l'autre file. Un point important est qu'en général, une seule application effectue un rendu 3D, typiquement un jeu vidéo en plein écran ou en fenêtré. Les autres applications utilisent plutôt le GPGPU ou des commandes de rendu 2D, qui utilisent uniquement les processeurs de shaders. En conséquence, pas besoin d'avoir plusieurs processeurs de commande généralistes. L'idéal est de n'avoir qu'un seul processeur de commandes graphiques, accompagné de pleins de processeurs de commandes dédiés au GPGPU. Cela permet d'exécuter des commandes graphiques et GPGPU en parallèle. Les priorités étaient autrefois statiques, à savoir que certaines processeurs de commande avaient toujours la priorité sur tous les autres. En théorie, le processeur de commande graphique devrait avoir la priorité sur les autres. Mais il y a des exceptions. Par exemple, sur les cartes graphiques avant l'architecture AMD RDNA 1, c'était l'inverse : les tâches de GPGPU avaient la priorité sur le rendu graphique. Maintenant, les GPU récents ont un système de priorité plus complexe, dynamique, qui choisit les priorité en fonction des besoins. ===L'usage de plusieurs processeurs de commande pour le rendu 3D=== Utiliser plusieurs applications est une première solution pour utiliser plusieurs files de commande. Mais avec un peu d'huile de coude, il est possible d'extraire plusieurs files de commande par application. le rendu 3D permet ce genre de choses, car de nombreux ''draw calls'' sont indépendants. Pour comprendre en quoi traiter plusieurs commandes peut être utile, je vais reprendre l'exemple décris sur cet [https://therealmjp.github.io/posts/breaking-down-barriers-part-3-multiple-command-processors/ article de blog], les liens sur l'ensemble des posts sur le sujet sont à la fin de ce chapitre. En rendu 3D, il est fréquent que des ''draw call'' consécutifs soient dépendants, qu'ils doivent s'exécuter l'un après l'autre, sans recouvrement possible. Un exemple est celui des filtres de post-traitement comme le Bloom ou la profondeur de champ. Ces deux filtres se font en plusieurs étapes, qui se traduisent en plusieurs ''draw call'' consécutifs. Les ''draw calls'' d'un filtre sont dépendants, à savoir que chaque étape lit le résultat de l'étape précédente. Ils sont donc séparés par des barrières GPU, ce qui ruine rapidement les performances. De plus, leurs ''draw calls'' n'utilisent pas tous les processeurs de shaders. Un filtre de bloom basique laisse des processeurs de shaders libre, qui pourraient être utilisés pour exécuter un autre filtre de post-traitement en parallèle, comme un filtre de profondeur de champ. Le problème est que la file de commande est séquentielle par nature. Une solution serait pour le moteur graphique de mélanger les ''draw call'' pour le filtre de bloom et ceux du filtre de profondeur de champ, mais ce serait assez compliqué pour les programmeurs. Une autre solution délègue le problème au matériel et à l'API. L'idée est d'avoir deux files de ''thread'' : une pour le filtre de Bloom, une autre pour le filtre de profondeur de champ. Les processeurs de shaders peuvent prendre des commandes dans les deux files. S'il y a assez de processeurs de shaders de libre, ils peuvent piocher des commandes dans la seconde file de commande. Les deux files accumulent des commandes, séparées par des barrières GPU. Mais les barrières GPU se limitent à l'intérieur d'une file de commande, elles n'ont pas d'impact sur l'autre file de commande. Mais qui dit deux files de commande dit : deux processeurs de commande et files de commandes ! Les deux processeurs de commande alimentent les deux files de ''thread'', en piochant chacun dans une file de commande dédiée. Et la même logique vaut pour N processeurs de commandes : tant qu'il y a autant de files de ''thread'' et de files de commande, la logique fonctionne à l'identique. La seule contrainte est d'alimenter plusieurs files de commande, ce qui est le job du pilote du GPU. De plus, il faut ajouter des ''commandes de synchronisation entre files de commandes'', qui permettent de synchroniser les deux files de commande. La plus simple force à attendre que les deux files de commandes soient vidées avant de démarrer une nouvelle commande. ===Le support dans les API graphiques modernes=== Avant l'apparition des API modernes Vulkan et DirectX 12, l'usage de plusieurs files de commande était peu fréquent. En effet, les applications ne voyaient pas de file de commande, les API graphiques avaient juste des ''draw calls'' et les commandes graphiques étaient envoyées au pilote une par une. C'est ce dernier qui remplissait la file de commande avec des commandes matérielles. En pratique, le pilote pouvait utiliser une file de commande par application, guère plus. Il n'y avait aucun moyen d'utiliser plusieurs files de commande par application. Pire que ça : un moteur graphique ne pouvait pas utiliser plusieurs cœurs. Les API graphiques de l'époque étaient séquentielles par nature, elles exécutaient les ''draw calls'' l'un après l'autre, et il ne pouvait y avoir qu'une seule instance par application. DirectX 11 a bien tenté d'ajouter des mécanismes pour multi-threader les moteurs graphiques, mais ils étaient difficiles à utiliser. Les jeux vidéo de l'époque étaient capables d'utiliser plusieurs cœurs, mais le moteur graphique n'en utilisait qu'un seul. Typiquement, le rendu du son était réalisé sur un cœur CPU, le reste sur un autre. Parfois, on séparait le moteur physique et le moteur graphique sur deux cœurs séparés, mais pas plus. Pour aider les programmeurs, les API modernes Vulkan et DirectX 12, gèrent nativement plusieurs files de commandes, qui sont exposées au programmeur. Ce qui était autrefois le domaine du pilote du GPU a été déporté dans les API graphiques. Les commandes remplacent les ''draw calls'' mais fonctionnent plus ou moins de la même manière. Le programmeur a accès à une fonction pour créer une file de commande, une autre pour ajouter une commande dedans, et une pour envoyer la file de commande au pilote de GPU. Le programmeur peut remplir ces files de commande comme il le souhaite, tant que les commandes dans des files séparées sont indépendantes. Il faut noter que ce sont des files de ''commandes graphiques'', pas des files de ''commande matérielles''. En clair, les files de commandes sont envoyées au pilote du GPU, qui les traduit en commandes matérielles. Et le GPU gère ses propres files de commandes matérielles. Il peut envoyer ses files de commandes séparément dans des processeurs de commande séparés, mais il peut décider d'accumuler toutes les commandes dans une seule file de commande matérielle si le GPU n'a qu'un seul processeur de commande, ou n'utiliser qu'une seule file de commande par application. En théorie, ce système permet de réduire l'usage du processeur. Au lieu d'appeler le pilote de GPU à chaque ''draw call'', il peut accumuler plein de ''draw call'' dans une file de commande et envoyer le tout en une seule fois au pilote. Il est donc possible de lancer un grand nombre de ''draw calls'' sans trop surcharger le CPU. Du moins en théorie, car le problème des ''draw call'' très petits que le GPU exécute trop vite reste présent. Mieux que ça, ce système permet de couper le moteur graphique en plusieurs ''threads'', afin de l'exécuter sur plusieurs cœurs. Par exemple, deux ''threads'' peuvent créer deux files de commandes différentes qui sont exécutées en parallèle (si elles ne sont pas sérialisées par le pilote de GPU). Tout complexifie la tâche du programmeur, vu qu'il doit faire le travail autrefois pris en charge par le pilote de GPU. L'intérêt est que cela permet au programmeur d'optimiser. Il peut utiliser plusieurs processeurs de commande par application, peut exécuter des commandes en parallèle pour remplir tous les processeurs de shaders, etc. Et surtout, il peut insérer des barrières GPU seulement quand elles sont nécessaires. Avec DirectX 11, le pilote de GPU avait tendance à être prudent. Il insérait des barrières GPU très souvent, et n'avait pas moyen de savoir lesquelles étaient réellement nécessaires et celles qui étaient juste inutiles. Il n'avait pas accès aux algorithmes des programmeurs, pour optimiser le tout. Les programmeurs ont la possibilité d'être plus efficaces, du moins s'ils sont assez compétents et qu'on leur laisse le temps. Il faut impérativement que les files puissent exécuter des commandes séparément sans que cela pose problème. Il y a bien des barrières GPU inter-files, mais laissons cela de côté. Toujours est-il que les files de commandes en question accumulent des commandes graphiques, pas des commandes matérielles. Mais le pilote de la carte graphique reçoit ces files de commande graphique et les accumule dans ses propres files de commandes matérielles. Avec DirectX 12, trois types différents de files de commande sont nativement supportés : GRAPHIC, COMPUTE et COPY. COPY correspond aux transferts DMA ou à des copies de données en VRAM, COMPUTE mémorise des commandes GPGPU, GRAPHIC est une file de commande de rendu 2D/3D. Du moins, c'est l'explication simplifiée. Car en réalité, les commandes GPGPU sont capables de faire tout ce que COPY permet, et les commandes GRAPHIC supportent tout ce que les commandes COMPUTE supportent. Les relations entre les trois sont inclusives : COPY est inclus dans COMPUTE, qui est lui-même inclus dans GRAPHIC. Les applications envoient des files de commande COPY, COMPUTE et GRAPHIC au pilote de GPU, qui décide quoi en faire. Si le GPU le supporte, il envoie les commandes GRAPHIC au processeur de commandé généraliste, les commandes COMPUTE à ceux dédiés au GPGPU, les commandes COPY au contrôleur DMA. Mais il peut aussi envoyer transformer des commandes COMPUTE en commandes GRAPHICS ou des commandes CPY en COMPUTE ou GRAPHIC, si le besoin s'en fait sentir. Si le GPU n'a qu'un seul processeur de commande, les commandes sont simplement remises en série et envoyées au seul processeur de commande. C'est le cas sur les GPU Intel intégrés : ils ont un simple processeur de commande qui fait tout. Ils ne gèrent pas les transferts DMA, car ils sont reliés à la RAM système (mémoire unifiée). Vulkan utilise un système différent. Vulkan permet de demander à la carte graphique combien elle a de processeurs de commande et quelles commandes ils supportent. Si un GPU n'a qu'un seul processeur de commande, Vulkan n'en verra qu'un et le code devra être adapté pour. En clair, la gestion des processeurs de commande est totalement délégué au programmeur. Il n'y a pas d'abstraction comme avec DirectX 12, mais une gestion fine du matériel. ==Sources extérieures== Pour compléter la lecture de ce chapitre, vous pouvez lire les 6 articles de blog suivants : * [https://therealmjp.github.io/posts/breaking-down-barriers-part-1-whats-a-barrier/ What's a barrier]. * [https://therealmjp.github.io/posts/breaking-down-barriers-part-2-synchronizing-gpu-threads/ Synchronizing GPU Threads]. * [https://therealmjp.github.io/posts/breaking-down-barriers-part-3-multiple-command-processors/ Multiple Command Processorsr]. * [https://therealmjp.github.io/posts/breaking-down-barriers-part-4-gpu-preemption/ GPU Preemption]. * [https://therealmjp.github.io/posts/breaking-down-barriers-part-5-back-to-the-real-world/ Back To The Real World]. * [https://therealmjp.github.io/posts/breaking-down-barriers-part-6-experimenting-with-overlap-and-preemption/ Experimenting With Overlap and Preemption]. {{NavChapitre | book=Les cartes graphiques | prev=Le processeur de commandes | prevText=Le processeur de commandes | next=Le pipeline géométrique : évolution | nextText=Le pipeline géométrique : évolution }} {{autocat}} 171tf57swh1hovsbwq0jjjcmwn08bxz Mathc initiation/a458 0 80896 763519 763315 2026-04-12T09:01:49Z Xhungab 23827 763519 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] : [[Mathc initiation/005s| Sommaire]] : {{Partie{{{type|}}}| L'intégrale de flux de surface définie paramétriquement simplifiée}} : En mathématiques, une intégrale de surface est une intégrale définie sur toute une surface qui peut être courbe dans l'espace. Pour une surface donnée, on peut intégrer sur un champ scalaire ou sur un champ vectorie [https://fr.khanacademy.org/math/multivariable-calculus/integrating-multivariable-functions/surface-parametrization/v/introduction-to-parametrizing-a-surface-with-two-parameters Khanacademy : introduction to parametrizing a surface with two parameters] ... ... ... [https://fr.khanacademy.org/math/multivariable-calculus/integrating-multivariable-functions/surface-integrals-introduction/v/introduction-to-the-surface-integral Khanacademy : introduction to the surface integral] : L''''intégrale de surface''' définie paramétriquement : (b (v(s) int( int( ||R_s x R_t|| dtds = (a (u(s) L''''intégrale de flux de surface''' définie paramétriquement : (b (v(s) int( int( f(Rx(s,t), Ry(s,t), Rz(s,t)) . |R_s x R_t| '''||R_s x R_t||''' dtds = (a (u(s) '''||R_s x R_t||''' L''''intégrale de flux de surface''' définie paramétriquement '''simplifiées''' : (b (v(s) int( int( f(Rx(s,t), Ry(s,t), Rz(s,t)) . |R_s x R_t| dtds = (a (u(s) <br> Copier la bibliothèque dans votre répertoire de travail : * [[Mathc initiation/a451|x_afile.h ............ Déclaration des fichiers h]] * [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]] * [[Mathc initiation/Fichiers c : c47ca|x_strcp.h ........... Déclaration des structures (points, vecteurs)]] * [[Mathc initiation/Fichiers h : c25a4|x_fxy.h .............. Calculer les dérivées partielles]] * [[Mathc initiation/a452|x_rsxrt.h ............ Le produit en croix]] * [[Mathc initiation/a453|x_pfs_ts.h ......... L'intégrale de flux de surface]] * [[Mathc initiation/a454|x_pfs_st.h ......... L'intégrale de flux de surface]] : <br> les fonctions f : * [[Mathc initiation/a455|f.h]] : <br> Exemples d'application : * [[Mathc initiation/a456|c00a.c .... dtds ]] * [[Mathc initiation/a457|c00b.c .... dsdt ]] {{AutoCat}} hveeu78st62b2mmt9e7a3k8c6cxt2pf 763520 763519 2026-04-12T09:03:50Z Xhungab 23827 763520 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] : [[Mathc initiation/005s| Sommaire]] : {{Partie{{{type|}}}| L'intégrale de flux de surface définie paramétriquement simplifiée}} : En mathématiques, une intégrale de surface est une intégrale définie sur toute une surface qui peut être courbe dans l'espace. Pour une surface donnée, on peut intégrer sur un champ scalaire ou sur un champ vectorie [https://fr.khanacademy.org/math/multivariable-calculus/integrating-multivariable-functions/surface-parametrization/v/introduction-to-parametrizing-a-surface-with-two-parameters Khanacademy : introduction to parametrizing a surface with two parameters] ... ... ... [https://fr.khanacademy.org/math/multivariable-calculus/integrating-multivariable-functions/surface-integrals-introduction/v/introduction-to-the-surface-integral Khanacademy : introduction to the surface integral] : L''''intégrale de surface''' définie paramétriquement : (b (v(s) int( int( ||R_s x R_t|| dtds = (a (u(s) L''''intégrale de flux de surface''' définie paramétriquement : (b (v(s) int( int( f(Rx(s,t), Ry(s,t), Rz(s,t)) . |R_s x R_t| '''||R_s x R_t||''' dtds = (a (u(s) '''||R_s x R_t||''' L''''intégrale de flux de surface''' définie paramétriquement '''simplifiées''' : (b (v(s) int( int( f(Rx(s,t), Ry(s,t), Rz(s,t)) . |R_s x R_t| dtds = (a (u(s) <br> Copier la bibliothèque dans votre répertoire de travail : * [[Mathc initiation/a451|x_afile.h ............ Déclaration des fichiers h]] * [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]] * [[Mathc initiation/Fichiers c : c47ca|x_strcp.h ........... Déclaration des structures (points, vecteurs)]] * [[Mathc initiation/Fichiers h : c25a4|x_fxy.h .............. Calculer les dérivées partielles]] * [[Mathc initiation/a452|x_rsxrt.h ............ Le produit en croix]] * [[Mathc initiation/a453|x_pfs_ts.h ......... L'intégrale de flux de surface]] '''définie paramétriquement simplifiées''' : * [[Mathc initiation/a454|x_pfs_st.h ......... L'intégrale de flux de surface]] : <br> les fonctions f : * [[Mathc initiation/a455|f.h]] : <br> Exemples d'application : * [[Mathc initiation/a456|c00a.c .... dtds ]] * [[Mathc initiation/a457|c00b.c .... dsdt ]] {{AutoCat}} l13uud2xugn22nog78c2mb3u8trcaae Les cartes graphiques/La microarchitecture des processeurs de shaders 0 81538 763456 763328 2026-04-11T15:58:35Z Mewtow 31375 /* L'Operand Collector et les caches de register reuse */ 763456 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 shaders | prevText=Les processeurs de shaders | next=Les caches d'un processeur de shader | netxText=Les caches d'un processeur de shader }}{{autocat}} gb5wj9kz20sfyzewgpkwdjq1njjjhtt Les cartes graphiques/Avant les GPUs : les cartes accélératrices 3D 0 81913 763451 762180 2026-04-11T14:15:16Z Mewtow 31375 /* La performance de l'unité géométrique */ 763451 wikitext text/x-wiki Dans ce chapitre, nous allons voir l'architecture de base d'une carte accélératrice 3D, et voir quelle est la distinction entre une carte accélératrice et un GPU. Dans ce chapitre, nous allons faire le lien avec le rendu tel que décrit dans le chapitre précédent. Les cartes graphiques modernes implémentent des circuits programmables, qui seront partiellement laissé de côté dans ce chapitre. Nous allons aussi nous concentrer sur les cartes graphiques à placage de texture inverse, le placage de texture direct ayant déjà été abordé dans le chapitre précédent. ==L'architecture d'une carte graphique 3D== Une carte accélératrice 3D est un carte d'affichage à laquelle on aurait rajouté des circuits de rendu 3D. Elle incorpore donc tous les circuits présents sur une carte d'affichage : un VDC, une interface avec le bus, une mémoire vidéo, des circuits d’interfaçage avec l'écran, un contrôleur DMA, etc. Le VDC s'occupe de l'affichage et éventuellement du rendu 2D, mais ne s'occupe pas du traitement de la 3D. Du moins, c'est le cas sur les cartes à placage de texture inverse. Le placage de texture direct utilise au contraire un VDC avec accélération 2D très performant, comme nous l'avons vu au chapitre précédent. Mais nous mettons ce cas particulier de côté. La carte accélératrice 3D reçoit des commandes graphiques, qui proviennent du pilote de la carte graphique, exécuté sur le processeur. les commandes en question sont très variées, avec des commandes de rendu 3D, de rendu 2D, de décodage/encodage vidéo, des transferts DMA, et bien d'autres. Mais nous allons nous concentrer sur les commandes de rendu 3D, qui demandent à la carte accélératrice 3D de faire une opération de rendu 3D. Pour cela, elles précisent quel tampon de sommet utiliser, quelles textures utiliser, quels shaders sont nécessaires, etc. La carte accélératrice 3D traite ces commandes grâce à deux circuits : des circuits de rendu 3D, et un chef d'orchestre qui dirige ces circuits de rendu pour qu'ils exécutent la commande demandée. Le chef d'orchestre s'appelle le '''processeur de commandes''', et il sera vu en détail dans quelques chapitres. Pour le moment, nous allons juste dire qu'il s'occupe de la logistique, de la répartition du travail. Pour les commandes de rendu 3D, il commande les différentes étapes du pipeline graphique et s'assure que les étapes s’exécutent dans le bon ordre. [[File:Architecture globale d'une carte 3D.png|centre|vignette|upright=2|Architecture globale d'une carte 3D]] Les circuits de rendu 3D regroupent des circuits hétérogènes, aux fonctions fort différentes. Dans le cas le plus simple, il y a un circuit pour chaque étape du pipeline graphique. De tels circuits sont appelés des '''unités de traitement graphique'''. On trouve ainsi une unité pour le placage de textures, une unité de traitement de la géométrie, une unité de rasterization, une unité d'enregistrement des pixels en mémoire appelée ROP, etc. Les anciennes cartes graphiques fonctionnaient ainsi, mais on verra que les cartes graphiques modernes font un petit peu différemment. Pour simplifier les explications, nous allons séparer la carte graphique en deux gros circuits bien distincts. En réalité, ils sont souvent séparés en sous-circuits plus petits, mais laissons cela de côté pour le moment. * Les '''unités géométriques''' pour les calculs géométriques ; * Les '''pipelines de pixel''' qui rastérisent l'image, plaquent les textures, et autres. Les unités géométriques manipulent des triangles, sommets ou polygones, donc des données géométriques. Les unités de pixel font tout le reste, mais le gros de leur travail est de manipuler des pixels ou des texels. Les unités géométriques sont soit des processeurs de ''shaders'' dédiés, soit des circuits fixes (non-programmables). Leur conception a beaucoup évolué dans le temps. Les toutes premières cartes graphiques, dans les années 80 et 90, utilisaient des processeurs dédiés, programmés avec un ''firmware'' dédié. Les cartes grand public du début des années 2000 utilisaient quant à elle des circuits fixes, non-programmables. Et par la suite, les cartes ultérieures sont revenues à des processeurs, mais cette fois-ci programmables directement avec des ''shaders'' et non un ''firmware''. Les pipelines de pixels, quant à eux, ont eu une évolution bien plus simple. Avant le milieu des années 2000, elles étaient réalisées par des circuits fixes, non-programmables. Il y avait bien quelques exceptions, mais c'était la norme. Ce n'est qu'avec l'arrivée des ''pixel shaders'' que les pipelines de pixels sont devenus programmables. Ils ont alors été implémentés avec plusieurs circuits, dont un processeur de shaders et d'autres circuits non-programmables. Et il est intéressant de voir quels sont ces circuits. ===Les circuits de traitement des pixels=== Parlons un peu plus en détail des pipelines de pixels. Pour mieux comprendre ce qu'elles font, il est intéressant de regarder ce qu'il y a dans un pipeline de pixel. Un pipeline de pixel effectue plusieurs opérations les unes à la suite, dans un ordre bien précis. Et cela explique l'usage du terme "pipeline" pour les désigner. Et ces opérations sont souvent réalisées par des circuits séparés, qui sont : * Un '''rastériseur''' qui fait le lien entre triangles et pixels ; * Une '''unité de texture''' qui lit les textures et les plaque sur les modèles 3D ; * Un '''ROP''' (''Raster Operation Pipeline''), qui gère grossièrement le tampon de profondeur (''z-buffer''). Le circuit de '''rastérisation''' prend en charge la rastérisation proprement dite. Pour rappel, la rastérisation projette une scène 3D sur l'écran. Elle fait passer d'une scène 3D à un écran en 2D avec des pixels. Lors de la rastérisation, chaque sommet est associé à un ou plusieurs pixels, à savoir les pixels qu'il occupe à l'écran. Elle fournit aussi diverses informations utiles pour la suite du pipeline graphique : la profondeur du sommet associé au pixel, les coordonnées de textures qui permettent de colorier le pixel. L'étape de '''placage de texture''' lit la texture associée au modèle 3D et identifie le texel adéquat avec les coordonnées textures, pour colorier le pixel. On travaille pixel par pixel, on récupère le texel associé à chaque pixel. Soit l'inverse du placage de texture direct, qui traversait une texture texel par texel, pour recopier le texel dans le pixel adéquat. Après l'étape de placage de textures, la carte graphique enregistre le résultat en mémoire. Lors de cette étape, divers traitements de '''post-traitement''' sont effectués et divers effets peuvent être ajoutés à l'image. Un effet de brouillard peut être ajouté, des tests de profondeur sont effectués pour éliminer certains pixels cachés, l'antialiasing est ajouté, on gère les effets de transparence, etc. Un chapitre entier sera dédié à ces opérations. [[File:Unité post-géométrie d'une carte graphique sans elimination des surfaces cachées.png|centre|vignette|upright=1.5|Unité post-1.5éométrie d'une carte graphique sans elimination des surfaces cachées]] ===Les circuits d'élimination des pixels cachés=== L'élimination des surfaces cachées élimine les triangles invisibles à l'écran, car cachés par un objet opaque. En théorie, elle est prise en charge à la toute fin du pipeline, dans les ROPs, car cela permet de gérer la transparence. En effet, on ne sait pas si une texture transparente sera plaquée sur le triangle ou non. En clair, on doit éliminer les triangles invisibles après le placage de textures, et donc dans les ROP. Les ROPs se chargent à la fois de l’élimination des pixels cachées et de la transparence, les deux s’influençant l'un l'autre. [[File:Unité post-géométrie d'une carte graphique avec elimination des surfaces cachées dans les ROPs.png|centre|vignette|upright=2|Unité post-géométrie d'une carte graphique avec élimination des surfaces cachées dans les ROPs]] Il y a cependant des cas où on sait d'avance que les textures ne sont pas transparentes. Dans ce cas, la carte graphique utilise les circuits d'élimination des pixels cachés juste après la rastérisation. Cela permet d'éliminer à l'avance les triangles dont on sait qu'ils ne seront pas rendus. [[File:Unité post-géométrie d'une carte graphique.png|centre|vignette|upright=2|Unité post-géométrie d'une carte graphique]] Les deux possibilités coexistent sur les cartes graphiques modernes. Une carte graphique moderne peut éliminer les surfaces cachées avant et après la rastérisation, grâce à des techniques d''''''early-z''''' dont nous parlerons plus tard, dans un chapitre dédié sur la rastérisation. ==Les circuits d'éclairage== [[File:Implémentation de l'éclairage sur les cartes graphiques.png|vignette|Implémentation de l'éclairage sur les cartes graphiques]] Les explications précédentes décrivent une carte graphique très simple, qui ne gère pas les techniques d'éclairage. Mais elles ont disparues depuis plusieurs décennies, toutes les cartes graphiques gèrent l'éclairage en matériel depuis les années 2000. Et ces GPU des années 2000 géraient différemment l'éclairage par pixel et l'éclairage par sommet. Pour rappel, l'éclairage par sommet attribue une couleur et une luminosité à chaque sommet. L'éclairage par pixel est plus fin, car il attribue une luminosité pour chaque pixel de l'écran. Les deux étaient gérés autrefois dans des circuits distincts, comme illustré ci-contre. ===Les circuits d'éclairage par sommet=== L''''éclairage par sommet''' est grossièrement calculé dans l'unité géométrique, le circuit de calculs géométriques. L’unité de traitement géométrique peut se mettre en œuvre de deux manières. * La première utilise un circuit non-programmable, appelé le '''circuit de ''Transform & Lightning''''', qui effectue les calculs d'éclairage par sommet (d'où le L de T&L), en plus des calculs de transformation (le T de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. * Une seconde solution utilise un processeur dédié, qui exécute tous les calculs géométriques. Pour cela, il faut fournir un programme qui émule le pipeline géométrique, appelé un '''''vertex shader''''', dont nous reparlerons d'ici quelques chapitres. Intuitivement, on se dit que l'unité géométrique calcule une luminosité pour chaque triangle/sommet, comprise entre 0 (très sombre) et 1 (très brillant). Mais en réalité, l'unité de traitement géométrique calcule une couleur RGB pour chaque sommet/triangle, cette '''couleur de sommet''' indiquant quelle est sa luminosité. L'avantage est que cela simplifie la combinaison avec les textures et permet d'avoir des lumières colorées. L'unité de traitement géométrique calcul donc une couleur de sommet, qui est envoyée à l'unité de rastérisation. L'unité de rastérisation calcule la couleur du pixel à partir des trois couleurs de sommet. Pour cela, il y a deux méthodes principales, qui correspondent à l'éclairage plat et l'éclairage de Gouraud, qu'on a vu dans le chapitre précédent. La première méthode attribue la même couleur à chaque pixel d'un triangle, typiquement la moyenne des trois couleurs de sommet. La seconde méthode, celle de l'éclairage de Gouraud, calcule une couleur différente pour chaque pixel du triangle. Le calcul en question est une interpolation, à savoir une sorte de moyenne pondérée. L'éclairage de Gouraud demande donc d'ajouter un circuit d'interpolation pour les couleurs des sommets. Il fait normalement partie du circuit de rastérisation, comme on le verra plus tard dans le chapitre dédié. Pour donner un exemple, la console de jeu Playstation 1 gérait l'éclairage de Gouraud directement en matériel, mais seulement partiellement. Elle n'avait pas de circuit de T&L, ni de ''vertex shaders'', mais intégrait un circuit pour interpoler les couleurs de chaque sommet. Enfin, il faut prendre en compte les textures. Pour cela, le pixel texturé est multiplié par la luminosité/couleur calculée par l'unité géométrique. Il y a donc un '''circuit de combinaison''' situé après l'unité de texture qui effectue la combinaison/multiplication. Le circuit de combinaison est parfois configurable, à savoir qu'on peut remplacer la multiplication par une addition ou d'autres opérations. Un tel circuit de combinaison s'appelle alors un '''''combiner''''', dans la vieille nomenclature graphique de l'époque des années 90-2000. [[File:Implémentation de l'éclairage par sommet avec des combiners.png|centre|vignette|upright=2|Implémentation de l'éclairage par sommet avec des combiners]] ===Les circuits d'éclairage par pixel=== L''''éclairage par pixel''' est implémenté d'une manière totalement différente. Une implémentation naïve ajoute un circuit d'éclairage par pixel dédié, après l'unité de texture. Le circuit d’éclairage par pixel n'utilise pas la couleur de sommet, mais d'autres informations nécessaires pour calculer la luminosité d'un pixel. Il a existé quelques rares cartes graphiques capables de faire de l'éclairage de Phong en matériel. Un exemple est celui de la Geforce 3, dont l'unité géométrique implémentait des instructions dédiées pour l'algorithme de Phong. L'unité géométrique de la Geforce 3 était programmable, et elle avait une instruction Phong, qui envoyait les normales au rastériseur. Les normales étaient alors interpolées par l'unité de rastérisation, puis utilisées par une unité d'éclairage par pixel dédié, fixe, non-programmable. La technique précédente doit être adaptée pour implémenter le ''bump-mapping'' et le ''normal-mapping'', qui mémorisent des informations d'éclairage dans une texture en mémoire vidéo. La texture contient des informations de relief pour le ''bump-mapping'', des normales précalculées pour le ''normal-mapping''. Pour cela, l'unité d'éclairage par pixel doit être reliée à l'unité de texture, mais l'implémentation matérielle n'est pas aisée. Un exemple de carte graphique capable de faire cela est celle de la Nintendo DS, la PICA200. Créée par une startup japonaise, elle incorporait un circuit de T&L, un éclairage de Phong, du ''cel shading'', des techniques de ''normal-mapping'', de ''Shadow Mapping'', de ''light-mapping'', du ''cubemapping'', de nombreux effets de post-traitement (bloom, effet de flou cinétique, ''motion blur'', rendu HDR, et autres). [[File:Implémentation de l'éclairage par pixel avec des combiners.png|centre|vignette|upright=2|Implémentation de l'éclairage par pixel avec des combiners]] De nos jours, les circuits d'éclairage par pixel ont été remplacés par un '''processeur de ''pixel shader'''''. Les processeurs de ''shaders'' sont des processeurs très simples, qui exécutent des algorithmes d'éclairage par pixel appelés des ''pixel shaders''. L'avantage est que les programmeurs peuvent coder l'algorithme d'éclairage de leur choix et l'exécuter sur le GPU. Pas besoin d'avoir une unité dédiée par algorithme d'éclairage, on a un processeur de shader à tout faire. Les processeurs de shaders récupèrent les pixels émis par le rastériseur, exécutent un ''pixel shader'' dessus, puis envoient le résultat à la suite du pipeline (aux ROPs). L'unité de texture est inclue dans le processeur de ''shader'', ce qui permet au processeur de shader de lire des textures en mémoire vidéo. Le processeur de shader peut faire ce qu'il veut avec les texels lus, cela va bien au-delà d'opérations de combinaison avec une couleur de sommet. Notez que cela permet de grandement faciliter l'implémentation du ''bump-mapping'' et du ''normal-mapping''. Sur les anciens GPUs, l'unité de texture était le seul moyen pour un processeur de shader d'accéder à la mémoire vidéo, ce qui faisait que les pixels shaders pouvaient lire des textures, rien de plus. Mais de nos jours, les processeurs de shaders sont directement connectés à la mémoire vidéo et peuvent lire ou écrire dedans sans passer par l'unité de texture, ce qui peut servir pour divers algorithmes complexes. [[File:Eclairage avec des pixels shaders.png|centre|vignette|upright=2|Eclairage avec des pixels shaders]] ==Les cartes graphiques avec plusieurs unités parallèles== Plus haut, nous avons décrit une carte graphique basique, très basique, avec seulement quatre unités. Une unité pour les calculs géométriques, un rastériseur, une unité pour les pixels/textures et un ROP. Cependant, les cartes graphiques ayant cette architecture sont très rares, pour ne pas dire inexistantes. Il n'est pas impossible que les toutes premières cartes graphiques aient suivi à la lettre cette architecture, mais même cela n'est pas sur. La raison : toutes les cartes graphiques dupliquent les circuits précédents pour gagner en performance, mais aussi pour s'adapter aux contraintes du rendu 3D. ===L'amplification des pixels et son impact sur les cartes graphiques=== Un triangle prend une certaine place à l'écran, il recouvre un ou plusieurs pixels lors de l'étape de rastérisation. Le nombre de pixels recouvert dépend fortement du triangle, de sa position, de sa profondeur, etc. Un triangle peut donner quelques pixels lors de l'étape de rastérisation, alors qu'un autre va couvrir 10 fois de pixels, un autre seulement trois fois plus, un autre seulement un pixel, etc. Le cas où un triangle ne recouvre qu'un seul pixel est rare, encore que la tendance commence à changer avec les jeux vidéos récents de la décennie 2020 utilisant l'Unreal Engine et la technologie Nanite. La conséquence est qu'il y a plus de travail à faire sur les pixels que sur les sommets, ce qui a reçu le nom d''''amplification des pixels'''. La conséquence est qu'une unité géométrique prendra un triangle en entrée, l'enverra au rastériseur, qui fournira en sortie un ou plusieurs pixels à éclairer/texturer. Et cette règle un triangle = 1,N pixels fait qu'il y a un déséquilibre entre les calculs géométriques et ce qui suit, que ce soit le placage de textures, l'éclairage par pixel ou l'enregistrement des pixels dans le ''framebuffer''. Et ce déséquilibre a un impact sur la manière dont un conçoit une carte graphique, ancienne comme moderne. S'il y a une seule unité de texture/pixels, alors le rastériseur envoie chaque pixel à texturer/éclairé un par un à l'unité de pixel. Le rastériseur produits ces pixels un par un, avec un algorithme adapté pour. L'unité géométrique attendra le temps que la rastérisation ait fini de traiter tous les pixels du triangle précédent. Elle calculera le prochain triangle pendant ce temps, mais cela ne fera que limiter la casse si beaucoup de pixels sont générés. Mais il est possible de profiter de l'amplification des pixels pour gagner en performances. L'idée est que le rastériseur produit plusieurs pixels en même temps, qui sont envoyés à plusieurs unités de texture et d'éclairage par pixel. Un exemple est illustré ci-dessous, avec une seule unité géométrique, mais quatre unités de texture, quatre unités d'éclairage par pixel, et quatre ROPs. Le rastériseur est conçu pour générer quatre pixels d'un seul coup si nécessaire. [[File:Architecture d'un GPU tenant compte de l'amplification des pixels.png|centre|vignette|upright=2.5|Architecture d'un GPU tenant compte de l'amplification des pixels]] La carte graphique précédente a des performances optimales quand un triangle recouvre 4 pixels : tout est fait en une seule passe. Si un triangle ne recouvre que 1, 2 ou 3 pixels, alors le rastériseur produira 1, 2 ou 3 et certaines unités suivant le rastériseur seront inutilisées. Mais si un triangle recouvre plus de 4 pixels, alors les pixels sont générés, texturés, éclairés et enregistrés en RAM par paquets de 4. En clair, la carte graphique peut s'adapter à l'amplification des pixels, mais pas parfaitement. Les GPU récents ont résolu partiellement ce problème avec un système de ''shaders'' unifiés, mais qu'on ne peut pas expliquer pour le moment. Pour donner un exemple du monde réel, les premières cartes graphique de l'entreprise SGI était de ce type. SGI a été une entreprise pinière dans le domaine du rendu en 3D, qui a opéré dans les années 80-90, avant de progressivement décliner et fermer. Elle a conçu de nombreux systèmes de type ''workstation'', donc destinés aux professionnels, avec des cartes graphiques dédiées. le grand public n'avait pas accès à ce genre de matériel, qui était très cher, vu qu'on n'était qu'au tout début de l'informatique. Nous ne détaillerons pas ces systèmes, car ils géraient leur mémoire vidéo d'une manière assez bizarre : elle était éclatée en plusieurs morceaux fusionnés chacun avec un ROP... Mais ils avaient tous une unité géométrique unique reliée à un rastériseur, qui alimentait plusieurs unités de texture/pixel et ROPs. Plus proche de nous, certaines cartes graphiques pour PC étaient aussi dans ce cas. Les toutes premières cartes graphiques pour PC n'avaient même pas de circuits géométriques, et se contentaient d'un rastériseur, d'unités de texture et de ROPs. Par la suite, la Geforce 256 a introduit une unité géométrique appelée l'unité de T&L. Les cartes graphiques de l'époque ont suivi le mouvement et ont aussi intégrée une unité géométrique presque identique. La Geforce 256 avait une unité géométrique, mais 4 unités de texture, 4 unités d'éclairage par pixel et 4 ROPs. ===Le multitexturing : dupliquer les unités de texture=== Le '''''multi-texturing''''' est une technique très importante pour le rendu 3D moderne. L'idée est de permettre à plusieurs textures de se superposer sur un objet. Divers effets graphiques demandent d'ajouter des textures par-dessus d'autres textures, pour ajouter des détails, du relief, sur une surface pré-existante. Un exemple intéressant vient des jeux de tir : ajouter des impacts de balles sur les murs. Pour cela, on plaque une texture d'impact de balle sur le mur, à la position du tir. Il s'agit là d'un exemple de ''decals'', des petites textures ajoutées sur les murs ou le sol, afin de simuler de la poussière, des impacts de balle, des craquelures, des fissures, des trous, etc. Le ''multi-texturing'' implique que calculer un pixel implique de lire plusieurs textures. En général, un pixel avec ''multi-texturing'' demande de lire deux textures, rarement plus. La carte graphique doit alors être capable d'accéder à deux textures en même temps, ou du moins faire semblant que. De plus, elle doit combiner les deux textures pour générer le pixel voulu, ce qui demande d'ajouter un circuit qui combine deux texels (des pixels de texture) pour donner un pixel. La solution la plus simple est de doubler les unités de texture et de combiner les textures dans l'unité d'éclairage par pixel. Résultat : pour une unité d'éclairage par pixel, on a deux unités de textures. La Geforce 2 et 3 utilisaient cette solution, dont le seul défaut est que la seconde unité de texture était utilisée seulement pour les objets sur lesquels le ''multi-texturing'' était utilisé. Les cartes ATI, le concurrent de l'époque de NVIDIA, aujourd'hui racheté par AMD, triplait les unités de texture. Mais cette possibilité était peu utilisée, la majorité des jeux se dépassant pas deux texture max par pixel. C'est sans doute pour cette raison que ce triplement a été abandonné à la génération suivante, les Radeon 9000 et 8500 se contentant de doubler les unités de texture. {|class="wikitable" |- ! Nom de la carte graphique !! Unités géométriques !! Unité de texture !! Unités de pixel !! ROPs |- ! Geforce 2 d'entrée de gamme | 1 || 2 || 4 || 2 |- ! Geforce 2 milieu/haut de gamme, Geforce 3 | 1 || 4 || 8 || 4 |- ! Radeon R100 bas de gamme | 1 || 1 || 3 || 1 |- ! Radeon R100 autres | 1 || 2 || 6 || 2 |} ===L'usage de plusieurs unités géométriques=== Pour encore augmenter les performances, il est possible d'utiliser plusieurs circuits de calcul géométriques, plusieurs unités géométriques. Et ce peu importe que ces unités soient des processeurs ou des circuits fixes non-programmables. Et pour cela, il existe deux grandes implémentations : utiliser plusieurs processeurs placés en série, ou les mettre en parallèle. Comprendre la première implémentation demande de faire quelques rappels sur les calculs géométriques. ====L'usage d'un pipeline géométrique proprement dit==== Pour rappel, le pipeline géométrique regroupe les quatre étapes suivantes : * L'étape de '''chargement des sommets/triangles''', qui sont lus depuis la mémoire vidéo et injectés dans le pipeline graphique. * L'étape de '''transformation''' effectue deux changements de coordonnées pour chaque sommet. ** Premièrement, elle place les objets au bon endroit dans la scène 3D, ce qui demande de mettre à jour les coordonnées de chaque sommet de chaque modèle. C'est la première étape de calcul : l'''étape de transformation des modèles 3D''. ** Deuxièmement, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * La phase d''''éclairage''' (en anglais ''lighting'') attribue une couleur à chaque sommet, qui définit son niveau de luminosité : est-ce que le sommet est fortement éclairé ou est-il dans l'ombre ? * La phase d''''assemblage des primitives''' regroupe les sommets en triangles. * Les phases de '''''clipping''''' ou le '''''culling''''' agissent sur des sommets/triangles/primitives, même si elles sont souvent regroupées dans l'étape de rastérisation. Si on met de côté le chargement des sommets/triangles, il est possible de faire tous ces calculs en bloc, dans un seul processeur ou une seule unité de T&L. Mais une autre idée, plus simple, attribue un processeur/circuit pour chaque étape. En faisant cela, on peut traiter plusieurs triangles/sommets en même temps, chacun étant dans une étape différente, chacun dans un processeur/circuit. Ceux qui auront déjà lu un cours d'architecture des ordinateurs reconnaitront la fameuse technique du pipeline, mais appliquée ici à un algorithme plus conséquent. Les processeurs sont en série, et chaque processeur reçoit les résultats du processeur précédent, et envoie son résultat au processeur suivant. Sauf en début ou en bout de chaine, évidemment. Pour donner un exemple, les premières cartes graphiques de SGI utilisaient 10/12 processeurs enchainés l'un à la suite de l'autre. Les 4 premiers géraient les étapes de transformation, les 6 suivants faisaient les opérations de clipping/culling, les deux derniers faisaient la rastérisation proprement dite. Pour lisser les transferts de données, il est possible d'ajouter des mémoires FIFOs entre les processeurs. Comme ça, si un processeur est bloqué par un calcul un peu trop long, cela ne bloque pas les processeurs précédents. A la place, le processeur précédent accumule des résultats dans la mémoire FIFOs, qui seront consommé ultérieurement. En théorie, on peut s'attendre à ce que la performance soit multipliée par le nombre de processeurs. En réalité, les étapes sont rarement équilibrées, certaines étapes prennent beaucoup plus de temps que les autres, ce qui fait que la répartition des calculs n'est pas idéale : certains processeurs attendent que le processeur suivant ait finit son travail. De plus, l'organisation en pipeline entraine des couts de transmission/communication entre étapes, notamment si on utilise des mémoires FIFOs entre processeurs, ce qui est toujours le cas. Cette implémentation n'a été utilisée que sur les toutes premières cartes graphiques, avant l'apparition des PC grand public. Les systèmes SGI, utilisés pour des stations de travail, utilisaient cette architecture, par exemple. Mais elle est totalement abandonnée depuis les années 90. ====L'usage de plusieurs unités géométriques en parallèle==== La seconde solution utilise plusieurs unités géométriques en parallèle. Chaque unité géométrique traite un triangle/sommet de bout en bout, en faisant transformation, éclairage, etc. Mais vu qu'il y en a plusieurs, on peut traiter plusieurs triangles/sommets : un dans chaque unité géométrique. C'est la solution retenue sur toutes les cartes graphiques depuis les années 90. Mais la présence de plusieurs unités géométriques a deux conséquences : il faut alimenter plusieurs unités géométriques en triangles/sommets, il faut gérer l'envoi des triangles au rastériseur. Les deux demandent des solutions distinctes. La répartition du travail sur les unités géométriques est déléguée au processeur de commandes. Il utilise les unités géométriques à tour de rôle : on envoie le premier triangle à la première unité, le second triangle à la seconde unité, le troisième triangle à la troisième, etc. Il s'agit de ce que l'on appelle l''''algorithme du tourniquet''', qui est assez efficace malgré sa simplicité. Il marche assez bien quand tous les triangles/sommets mettent approximativement le même temps pour être traités. Si le temps de calcul varie beaucoup d'un triangle/sommet à l'autre, une solution toute simple détecte quels sont les processeurs de shaders libres et ceux occupés. Il suffit alors d'appliquer l'algorithme du tourniquet seulement sur les processeurs de shaders libres, qui n'ont rien à faire. Un autre problème survient cette fois-ci en sortie des unités géométriques. Comment connecter plusieurs unités géométriques au reste de la carte graphique ? Évidemment, la carte graphique contient plusieurs unités de texture/pixel et plusieurs ROPs. Elle tient compte de l'amplification des pixels, ce qui fait qu'il y a moins d'unités géométriques que d'autres circuits, entre 2 à 8 fois moins environ. Pour créer une carte graphique avec plusieurs unités géométriques, il y a plusieurs solutions, que nous allons détailler dans ce qui suit. Pour les explications, nous allons prendre l'exemple de cartes graphiques avec 2 unités géométriques et 8 unités de texture/pixel, et autant de ROPs. La première solution serait simplement de dupliquer les circuits précédents, en gardant leurs interconnexions. Pour l'exemple, on aurait 2 unités géométriques, chacune connectée à 4 unités de textures/pixels. L'unité géométrique est suivie par un rastériseur qui alimente 4 unités de texture/pixel, comme c'était le cas dans la section précédente. L'implémentation est alors très simple : on a juste à dupliquer les circuits et à modifier le processeur de commande. Il faut aussi modifier les connexions des ROPs à la mémoire vidéo. Mais les interconnexions avec le rastériseur ne sont pas modifiées. Un désavantage est que l'amplification des pixels n'est pas gérée au mieux. Imaginez que l'on ait deux triangles à rastériser, qui génèrent 8 pixels en tout : un qui génère 6 pixels à la rastérisation, l'autre seulement 2. Il n'est pas possible de traiter les 8 pixels générés. Le triangle générant deux pixels va alimenter deux unités de texture/pixels et en laisser deux inutilisées, l'autre triangle sera traité en deux fois (4 pixels, puis 2). La duplication bête et méchante n'utilise donc pas à la perfection les unités de texture/pixel. Une autre solution permet de gérer à la perfection l'amplification des pixels. Elle consiste à utiliser un seul rastériseur à haute performance, sur lequel on connecte les unités géométriques et les unités de texture/pixel. L'idée est que le rastériseur peut recevoir N triangles à la fois et alimenter M unités de texture/pixels. Le rastériseur unique s'occupe de faire plusieurs rastérisations de triangles à la fois, et répartit automatiquement les pixels générés sur les unités de texture/pixel. Pour donner un exemple, le GPU Geforce 6800 de NVIDIA avait 6 unités géométriques, 16 unités faisant à la fois placage de textures et éclairage par pixel, et 16 ROPs. Un point important avec ce GPU est qu'il n'avait qu'un seul rastériseur, détail sur lequel on reviendra dans ce qui suit ! [[File:GeForce 6800.png|centre|vignette|upright=2.5|GeForce 6800, les unités géométriques sont ici appelées les ''vertex processor'', les unités de texture/pixel sont les ''fragment processors'', les ROPs sont les ''pixel blending units''.]] ==Les cartes graphiques en mode immédiat et à tuile== Il est courant de dire qu'il existe deux types de cartes graphiques : celles en mode immédiat, et celles avec un rendu en tuiles (''tiles''). Il s'agit là des deux types principaux de cartes graphiques à l'heure actuelle, mais quelques architectures faisaient autrement dans le passé. Une autre classification, plus générale, sépare les GPU en GPU ''sort-last'', ''sort-first'' et ''sort-middle''. Les GPU en mode immédiat correspondent aux GPU en mode immédiat, alors que le rendu à tuile est une sous-catégorie des GPU ''sort-middle''. La différence entre les deux est liée à la manière dont les pixels/primitives sont répartis sur l'écran. Les GPU ''sort-first'' ont plusieurs pipelines séparés, chacun traitant une partie de l'écran. Ils déterminent la position des triangles à l'écran, puis répartissent les triangles dans les pipelines adéquats. Par exemple, on peut imaginer un GPU ''sort-first'' avec quatre unités séparées, chacune traitant un quart de l'écran. Au tout début du rendu, une unité de répartition détermine la position d'un triangle à l'écran, et l'envoie à l'unité adéquate. Si le triangle est dans le coin inférieur gauche, il sera envoyé à l'unité dédiée à ce coin. S'il est situé au milieu de l'écran, il sera envoyé aux quatre unités, chacune ne traitant les pixels que pour son coin à elle. Les GPU ''sort-middle'' découpent l'écran en carrés de 4, 8, 16, 32 pixels de côté , qui sont rendus séparément les uns des autres. Les morceaux d'image en question sont appelés des ''tiles'' en anglais, mot que nous avons décidé de ne pas traduire pour ne pas le confondre avec les tuiles du rendu 2D. Il y a une assignation stricte entre une unité de pixel/texture et une ''tile''. Par exemple, sur un système avec deux unités de texture/pixel, la première unité traitera les ''tiles'' paires, l'autre unité les ''tiles'' impaires. Les GPU ''sort-last'' sont l'extrême inverse. Ils ont des unités banalisées qui se moquent de l'endroit où se trouve un pixel à l'écran. Leurs unités géométriques traitent des polygones sans se préoccuper de leur place à l'écran. Le rastériseur envoie les pixels aux unités de textures/ROPs sans se soucier de leur place à l'écran. Encore que quelques optimisations s'en mêlent pour profiter au mieux des caches de texture et des caches intégrés aux ROPs, mais l'essentiel est qu'il n'y a pas de répartition fixe. Il n'y a pas de logique du type : ce pixel ou ce triangle est à tel endroit à l'écran, on l'envoie vers telle unité de texture/ROP. Ce sont les ROPs qui se chargent d'enregistrer les pixles finaux au bon endroits dans le ''framebuffer''. La gestion de la place des pixels à l'écran se fait donc à la toute fin du pipeline, d'où le nom de ''sort-last''. Pour résumer, les trois types de GPU se distinguent suivant l'endroit où les triangles/pixels sont répartis suivant leur place à l'écran. Avec le ''sort-first'', ce sont les triangles qui sont triés suivant leur place à l'écran. Le tri a donc lieu avant les unités géométriques. Avec le ''sort-middle'', ce sont les fragments générés par la rastérisation qui sont triés suivant leur place à l'écran, d'où l'existence de ''tiles''. Le tri a lieu entre les unités géométriques et le rastériseur. Les unités géométriques se moquent de la place à l'écran des primitives qu'ils traitent, mais pas les rastériseurs et les unités de texture. Enfin, avec le ''sort-last'', ce sont les pixels finaux qui sont triés selon leur place à l'écran, seuls les ROPs se préoccupent de cette place à l'écran. Concrètement, les GPU de type ''sort-first'' sont très rares, l'auteur de ce cours n'en connait aucun exemple. Les deux autres types de GPU sont eux beaucoup plus communs. Reste à voir ce qu'il y a à l'intérieur d'un GPU ''sort-middle'' et/ou ''sort-last''. Pour simplifier les explications, nous allons regrouper les circuits de traitement des pixels dans un seul gros circuits appelé le rastériseur, par abus de langage. La carte graphique est donc composée de deux circuits : l'unité géométrique et le mal-nommé rastériseur. Les cartes graphiques ajoutent des mémoires caches pour la géométrie et les textures, afin de rendre leur accès plus rapide. [[File:Carte graphique, généralités.png|centre|vignette|upright=2|Carte graphique, généralités]] ===Les GPU ''sort-last'', en mode immédiat=== Les cartes graphiques en mode immédiat implémentent le pipeline graphique d'une manière assez évidente. L'unité géométrique envoie des triangles au rastériseur, qui lui-même envoie les pixels à l'unité de texture, qui elle-même envoie le pixel texturé au ROP. Elles effectuent le rendu 3D triangle par tringle, pixel par pixel. Un point important est que pendant que le pixel N est dans les ROP, les pixels N+1 est dans l'unité de texture, le pixel N+2 est dans le rastériseur et le triangle suivant est dans l'unité géométrique. En clair, on n'attend pas qu'un triangle soit affiché pour en démarrer un autre. Un problème est qu'un triangle dans une scène 3D correspond souvent à plusieurs pixels, ce qui fait que la rastérisation prend plus de temps de calcul que la géométrie. En conséquence, il arrive fréquemment que le rastériseur soit occupé, alors que l'unité de géométrie veut lui envoyer des données. Pour éviter tout problème, on insère une petite mémoire entre l'unité géométrique et le rastériseur, qui porte le nom de '''tampon de primitives'''. Elle permet d'accumuler les sommets calculés quand le rastériseur est occupé. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Le tout peut s'adapter à la présence de plusieurs unités géométriques, de plusieurs unités de texture ou processeurs de shaders, tant qu'on conserve un rastériseur unique. Il suffit alors d'adapter le tampon de primitive et le rastériseur. Si on veut rajouter des unités de texture ou des processeurs de pixel shaders, le tampon de primitives n'est pas concerné : il suffit que le rastériseur ait plusieurs sorties, une par unité de texture/pixel shader. Par contre, la présence de plusieurs unités géométriques impacte le tampon de primitive. Avec plusieurs unités géométriques, il y a deux solutions : soit on garde un tampon de primitive unique partagé, soit il y a un tampon de primitive par unité géométrique. Avec la première solution, toutes les unités géométriques sont reliées à un tampon de primitives unique. Le tampon de primitive est conçu pour qu'on puisse écrire plusieurs primitives dedans en même temps. Le rastériseur n'a pas à être modifié. Une autre solution utilise un tampon de primitive par unité géométrique. Le rastériseur peut alors piocher dans plusieurs tampons de primitive, ce qui demande de modifier le rastériseur. Il y a alors un système d'arbitrage, pour que le rastériseur pioche des primitive équitablement dans tous les tampons de primitive, pas question que l'un d'entre eux soit ignoré durant trop longtemps. ===Les GPU ''sort-middle'' des années 90=== Voyons maintenant les architectures ''sort-middle'' utilisée dans les années 80-90, à une époque où les cartes graphiques grand public n'existaient pas encore. Les cartes graphiques de l’entreprise SGI sont dans ce cas, mais aussi le Pixel Planes 5, et de nombreux autres systèmes graphiques. Elles utilisaient un rendu à ''tile'' assez original. Dans ce qui suit, nous allons décrire l'architecture des systèmes SGI, qui sont représentatifs. L'idée était que l'image était découpée en un nombre de ''tiles'' qui variait selon le système utilisé, mais qui était au minimum de 5 et pouvait aller jusqu'à 20. Et chaque ''tile'' avait sa propre unité de traitement, qui contenait un rastériseur, une unité de texture, un ROP, etc. En clair, la carte graphique contenait entre 5 et 20 unités de traitement séparées, chacune dédiée à une ''tile''. Les triangles sortant des unités géométriques étaient envoyés à toutes les unités de traitement, sans exception. Une fois le triangle réceptionné, l'unité de traitement déterminait si le triangle s'affichait dans la ''tile'' associée ou non. Si c'est le cas, le rastériseur rastérise le triangle, génère les pixels, les textures sont lues, puis le tout est enregistré en mémoire vidéo. Si ce n'est pas le cas, elle abandonne le polygone/triangle reçu. Si le triangle est partiellement dans la ''tile'', le rastériseur génère les pixels qui sont dans la ''tile'', par les autres. Précisons que les cartes de ce styles incorporaient un tampon de primitive, ce qui permettait de simplifier la conception de la carte graphique. Sur la carte ''Infinite Reality'', le tampon de primitive faisait 4 méga-octets de RAM, ce permettait de mémoriser 65 536 sommets. Sur la carte ''Reality Engine'', il y avait même plusieurs tampons de primitives, un par unité géométrique. Les polygones sortaient des unités géométriques, étaient accumulés dans les tampons de primitives, puis étaient ''broadcastés'' à toutes les unités de traitement. Pour cela, le bus en bleu dans le schéma précédent est en réalité un réseau ''crossbar'' avec un système de ''broadcast''. Une caractéristique de ces architectures est qu'elles mettent le ''framebuffer'' à part de la mémoire vidéo. De plus, ce ''framebuffer'' est lui-même découpée en ''tile''. Sur la carte ''Reality Engine'', le ''framebuffer'' est découpé en 5 à 20 sous-''framebuffer'', un par ''tile''. Et chaque mini-''framebuffer'' est placé dans l'unité de traitement de la ''tile'' associée ! Ainsi, au lieu de connecter 5-20 ROPs à une mémoire vidéo unique, chaque ROP contient une '''''RAM tile''''', qui mémorise la ''tile'' en cours de traitement. Évidemment, cela pose quelques problèmes pour la connexion au VDC, en raison de l'absence de ''framebuffer'' unique, mais rien d'insurmontable. L'architecture est illustrée ci-dessous. : Le Pixel Planes 5 avait un système similaire, mais avait en plus un ''framebuffer'' complet, dans lequel les sous-''framebuffer'' étaient recopiés pour obtenir l'image finale. [[File:Architecture des premières cartes graphiques SGI.png|centre|vignette|upright=2|Architecture des premières cartes graphiques SGI]] Un autre détail de l'architecture est lié à la mémoire pour les textures. Les concepteurs de SGI ont décidé de séparer les textures dans une mémoire à part du reste de la mémoire vidéo. Il n'y a pour ainsi dire pas de mémoire vidéo proprement dit : la géométrie à rendre est dans une mémoire à part, idem pour les textures, et pour le ''framebuffer''. On s'attendrait à ce que la mémoire de texture soit reliée aux 5-20 unités de texture, mais les concepteurs ont décidé de faire autrement. A la place, chaque unité de texture contient une copie de la mémoire de texture, qui est donc dupliquée en 5-20 exemplaires ! Difficile de comprendre la raison de ce choix, mais cela simplifiait sans doute les interconnexions internes de la carte graphique, au prix d'un cout en RAM assez important. ===Les GPU à rendu à ''tile''=== Les GPU de SGI, vus précédemment, disposent de d'une unité de traitement par ''tile''. Faire ainsi permet de nombreuses optimisations, comme éclater le ''framebuffer'' en plusieurs ''RAM tile''. Mais le cout en matériel est conséquent. Pour économiser des circuits, l'idéal serait d'utiliser moins d'unités de traitement pour les pixels/fragments/textures. Mais pour cela, il faut profondément modifier l'architecture précédente. On perd forcément le lien entre une unité de traitement et une ''tile''. Et cela impose de revoir totalement la manière dont les unités géométriques communiquent avec les unités de traitement. La solution retenue est celle des GPU à rendu en ''tile'' proprement dit, aussi appelés ''GPU TBR'' (''Tile Based Rendering''). Les plus simples n'utilisent qu'une seule unité de traitement et n'ont qu'une seule ''RAM tile''. En conséquence, les ''tiles'' sont rendues l'une après l'autre. Au lieu de rendre chaque triangle/polygone l'un après l'autre, la géométrie est intégralement rendue avant de faire la rastérisation. Les triangles sont enregistrés dans la mémoire vidéo et regroupés par ''tile'', avant la rastérisation. La mémoire vidéo contient donc plusieurs paquets de triangles, avec un paquet par ''tile''. Les paquets/''tiles'' sont envoyées au rastériseur un par un, la rastérisation se fait ''tile'' par ''tile''. La ''RAM tile'' existe toujours, même si son utilité est différente. La ''RAM tile'' accélère le rendu d'une ''tile'', car tout ce qui est nécessaire pour rendre une ''tile'' est mémorisé dedans : la ''tile'', le tampon de profondeur, le tampon de stencil et plein d'autres trucs. Pas besoin d’accéder à un gigantesque z-buffer pour toute l'image, juste d'un minuscule z-buffer pour la ''tile'' en cours de traitement, qui tient totalement dans la SRAM. : Il faut noter que les ''tiles'' sont généralement assez petites : 16 ou 32 pixels de côté, rarement plus. En comparaison, les ''tiles'' faisaient 128 pixels de côté pour les cartes de SGI. [[File:Carte graphique en rendu par tiles.png|centre|vignette|upright=2|Carte graphique en rendu par tiles]] Il est possible pour un GPU TBR de traiter plusieurs ''tiles'' en même temps, en parallèle, dans des unités séparées. Un exemple est celui du GPU ARM Mali 400, qui dispose d'une unité géométrique (un processeur de ''vertex''), mais 4 processeurs de pixels. Il peut donc traiter quatre ''tiles'' en même temps, chacune étant rendue dans un processeur de pixel dédié. Les 4 processeurs de pixels ont chacun leur propre ''RAM tile'' rien qu'à eux. La présence d'une ''RAM tile'' a de nombreux avantages et impacte grandement l'architecture de la carte graphique. En premier lieu, les ROPs sont drastiquement modifiés. De nombreux GPU TBR n'ont même pas de ROPs ! A la place, les ROPs sont émulés par les processeurs de pixel shader. Les ''pixel shaders'' peuvent lire ou écrire directement dans le ''framebuffer'', sur les GPU TBR, ce qui leur permet d'émuler les ROPs avec des instructions mathématique/mémoire. Le ''driver'' patche automatiquement les ''pixel shader'' pour ajouter de quoi émuler les ROPs à la fin des ''pixel shaders''. Cela garantit une économie de circuits non-négligeable. La présence d'une ''RAM tile'' fait que le tampon de profondeur disparait. Par contre, les GPU de type TBR doivent enregistrer les triangles en mémoire vidéo, et les trier par paquets. Cela compense partiellement, totalement, ou sur-compense, les économies liée à la ''RAM tile''. Le regroupement des triangles par ''tile'' s'accompagne de quelques optimisations assez sympathiques. Par exemple, les GPU TBR modernes peuvent trier les triangles selon leur profondeur, directement lors du regroupement en paquets. L'avantage est que cela permet à l'élimination des pixels cachés de fonctionner au mieux. L'élimination des pixels cachés fonctionne à la perfection quand les triangles sont triés du plus proche au plus lointain, pour les objets opaques. Les GPU en mode immédiat ne peuvent pas faire ce tri, mais les GPU TBR peuvent le faire, soit totalement, soit partiellement. Un autre avantage est que l’antialiasing est plus rapide. Pour ceux qui ne le savent pas, l'antialiasing est une technique qui améliore la qualité d’image, en simulant une résolution supérieure. Une image rendue avec antialiasing aura la même résolution que l'écran, mais n'aura pas certains artefacts liés à une résolution insuffisante. Et l'antialiasing a lieu dans et après la rastérisation, et augmente la résolution du tampon de profondeur et du z-buffer. Les GPU en mode immédiat disposent d'optimisations pour limiter la casse, mais les ROP font malgré tout beaucoup d'accès mémoire. Avec le rendu en tiles, l'antialising se fait dans la ''RAM tile'', n'a pas besoin de passer par la mémoire vidéo et est donc plus rapide. ===Des compromis différents=== Les cartes graphiques des ordinateurs de bureau ou portables sont toutes en mode immédiat, alors que celles des appareils mobiles, smartphones et autres équipements embarqués ont un rendu en ''tiles''. Les raisons à cela sont multiples, mais la principale est que le rendu en ''tiles'' marche beaucoup mieux pour le rendu en 2D, comparé aux architectures en mode immédiat, ce qui se marie bien aux besoins des smartphones et autres objets connectés. La performance d'une carte graphique est limitée par la quantité d'accès mémoire par seconde. Autant dire que les économiser est primordial. Et les cartes en mode immédiat et par tile ne sont pas égales de ce point de vue. En mode immédiat, le tampon de primitives évite de passer par la mémoire vidéo, mais le z-buffer et le ''framebuffer'' sont très gourmand en accès mémoire. Avec les architectures à tile, c'est l'inverse : la géométrie est enregistrée en mémoire vidéo, mais le tampon de profondeur n'utilise pas la RAM vidéo. Au final, les deux architectures sont optimisées pour deux types de rendus différents. Les cartes à rendu en tile brillent quand la géométrie n'est pas trop compliquée, et que la résolution est grande ou que l'antialising est activé. Les cartes en mode immédiat sont elles douées pour les scènes géométriquement lourdes, mais avec peu d'accès aux pixels. Le tout est limité par divers caches qui tentent de rendre les accès mémoires moins fréquents, sur les deux types de cartes, mais sans que ce soit une solution miracle. ==La performance des anciennes cartes graphiques 3D== Intuitivement, la performance d'une carte graphique dépend de la performance de chacun de ses circuits : processeur de commande, mémoire vidéo, circuits de rendu 3D, VDC, etc. En pratique, il est rare qu'on soit limité par le VDC ou le processeur de commande. Les seules limitations viennent des circuits de rendu 3D et de la mémoire vidéo. Nous ne pouvons pas aborder la performance de la mémoire vidéo pour le moment. Tout ce que l'on peut dire est qu'il faut qu'elle soit assez rapide pour alimenter le rendu 3D en données. Les circuits de rendu 3D doivent lire des triangles et textures en mémoire vidéo, qui doit être assez rapide pour ça et ne pas les faire attendre. Pour le reste, voyons la performance des circuits de rendu 3D. Il ne nous est là aussi pas possible de détailler ce qui impacte la performance d'un GPU moderne. Dès que des processeurs de shaders sont impliqués, parler de performance demande de connaitre sur le bout des doigts les processeurs de shaders, ce qu'on n'a pas encore vu à ce stade du cours. Par contre, on peut détailler ce qu'il en était pour les anciennes cartes 3D, sans processeurs de shaders. Elles contenaient des ROPs, des unités de texture, un rastériseur et une unité géométrique (l'unité de T&L). Étudions d'abord la performance des unités de texture et des ROPs. Cela nous permettra de parler d'un paramètre qui avait son importance sur les anciennes cartes graphiques, avant les années 2000 : le ''fillrate''. Le '''''fill rate''''', ou taux de remplissage, est une ancienne mesure de performance autrefois utilisée pour comparer les cartes graphiques entre elles. Il s'agit d'une mesure assez approximative, au même titre que la fréquence d'horloge. Concrètement, plus il est élevé, meilleures seront les performances, en théorie. Mais attention : les petites différences de ''fillrate'' ne suffisent pas à rendre un verdict. De plus, il existe deux types distincts de ''fillrate'' : le ''Texture Fillrate'' et le ''Pixel Fillrate''. Voyons d'abord le ''Pixel Fillrate''. ===Le ''pixel fillrate'' : la performance des ROPs=== Le '''''pixel fillrate''''' est le nombre maximal de pixels que la carte graphique peut écrire en mémoire vidéo par seconde. Il est exprimé en ''Méga-Pixels par seconde'' ou en ''Giga-Pixels par seconde'', souvent abréviés en GP/s et MP/s. C'est une unité que vous croisez sans doute pour la première fois et qui mérite quelques explications. Premièrement, dans méga-pixels par seconde, il y a mégapixels. Il s'agit d'une unité pour compter le nombre de pixels d'une image. Un mégapixel signifie tout simplement un million de pixels, un gigapixel signifie un milliard de pixels. Je précise bien un million et un milliard, ce ne sont pas des multiples de 1024, comme on est habitué à en voir en informatique. Le nombre de pixels d'une image augmente avec la résolution utilisée, mais il reste de l'ordre du mégapixel, guère plus. Voici un tableau avec les résolutions les plus utilisées et le nombre de pixels associé. {|class="wikitable" |- ! Résolution !! Nombre de pixels |- | colspan="2" | |- | colspan="2" | Résolutions anciennes en 4:3 |- | 640 × 480 || 307 200 <math>\approx</math> 0,3 MP |- | 800 × 600 || 480 000 = 0,48 MP |- | 1 024 × 768 || 786 432 <math>\approx</math> 0,8 MP |- | 1 280 × 960 || 1 228 800 <math>\approx</math> 1,2 MP |- | 1 600 × 1 200 || 1 920 000 = 1,92 MP |- | colspan="2" | |- | colspan="2" | Résolutions modernes en 16:9 |- | 1 920 × 1 080 || 2 073 600 <math>\approx</math> 2 MP |- | 3 840 × 2 160 (4k) || 8 294 400 <math>\approx</math> 8.3 MP |} Maintenant, regardons ce qui se passe si on veut rendre plusieurs images par secondes. Intuitivement, on se dit qu'il faudra un ''pixel fillrate'' minimal pour cela. Et il se trouve qu'on peut le calculer aisément. Prenons par exemple une image en 1600 × 1200, de 1,92 mégapixels. Si on veut avoir 60 images par secondes, avec cette résolution, cela fait 1,92 * 60 mégapixels par secondes. En clair, le ''pixel fillrate'' minimal se calcule en multipliant la résolution par le ''framerate''. Le ''pixel fillrate'' minimal tourne autour de la centaine de mégapixels par seconde, voire approche le gigapixel par seconde en haute résolution. Les images font entre 1 et 10 mégapixels, pour environ 100 FPS, l'intervalle colle parfaitement. Maintenant, comparons un peu avec ce dont sont capables les GPUs. Les toutes premières cartes graphiques commerciales avaient un ''pixel fillrate'' proche de la centaine de méga-pixels par seconde. Pour donner un exemple, la Geforce 256 avait un ''pixel fillrate'' de 480 MP/s, la Geforce 3 faisait entre 700 et 960 MP/s selon le modèle. De nos jours, le ''pixel fillrate'' est de l'ordre de la centaine de Gigapixels. Pour donner un exemple, les Geforce RTX 5000 ont un ''pixel fillrate'' de 82.3GP/s pour la RTX 5050, à 423.6 GP/S pour la RTX 5090. Les GPU ont un ''pixel fillrate'' qui dépasse de très loin la valeur minimale, ce qui est franchement étrange. La raison à cela est que le ''pixel fillrate'' minimal se calcule sous l'hypothèse que chaque pixel de l'image finale ne sera écrit qu'une seule fois. Mais dans les faits, il est fréquent qu'un pixel soit dessiné plusieurs fois avant d'obtenir l'image finale. La raison principale est liée aux surfaces cachées. Si un objet est derrière un autre, il arrive que celui-ci soit dessiné dans le ''framebuffer'', avant que l'objet devant soit re-dessiné par-dessus. Des pixels ont alors été écrits, puis ré-écrits. Le fait de dessiner un pixel plusieurs fois porte un nom. Il s'agit d'un phénomène d''''''overdraw''''', ou sur-dessinage en français. Le sur-dessinage fait que le ''pixel fillrate'' minimal ne suffit pas en pratique. Pour éviter tout problème, le ''pixel fillrate'' du GPU doit être supérieur au ''pixel fillrate'' minimal, d'environ un ordre de grandeur. L'élimination des surfaces cachées réduit l'''overdraw'', mais elle ne fait pas de miracles. En pratique, le sur-dessinage ne concerne qu'une partie assez mineure des pixels de l'image, et un pixel est rarement écrit plus d'une dizaine de fois. Et les GPus modernes ont un ''pixel fillrate'' tellement démentiel qu'il n'est presque jamais un facteur limitant. Le ''pixel fillrate'' d'un GPU dépend de plusieurs choses : le nombre de ROPs, leur fréquence d'horloge exprimée en MHz/GHz, la bande passante mémoire, et bien d'autres. En théorie, la bande passante mémoire n'est pas un point limitant, les concepteurs du GPU prévoient une mémoire suffisamment rapide pour qu'elle puisse encaisser le ''pixel fillrate'' maximal, tout en ayant encore de la marge pour lire des textures et la géométrie. En clair, le ''pixel fillrate'' est surtout dépendant des ROPs, de leur nombre, de leur vitesse, de leur implémentation. Le ''pixel fillrate'' du GPU est difficile à calculer, mais l'approximation la plus utilisée est la suivante. Elle part du principe qu'un ROP peut écrire un pixel par cycle d'horloge. Ce n'est pas forcément le cas, tout dépend de l'implémentation des ROPs. Certains GPU performants ont des ROPs capables d'écrire des blocs de 8*8 pixels d'un seul coup en mémoire vidéo, alors que d'anciens GPU font avec des ROPs limités, seulement capables d'écrire un pixel tout les 10 cycles d'horloge. Toujours est-il qu'avec cette hypothèse, le ''pixel fillrate'' est égal au nombre de ROPs, multiplié par leur fréquence d'horloge. Je précise "leur" fréquence d'horloge, car il est possible de faire fonctionner l'unité de T&L, les ROPs, les unités de texture et le rastériseur à des fréquences différentes. C'est parfaitement possible, le cout en performance est parfois assez faible, mais le gain en consommation d'énergie est souvent important. Et justement, il a existé des GPU sur lesquels les ROPs avaient une fréquence inférieure à celle du reste du GPU. Dans ce cas, c'est la fréquence des ROPs qui est importante. Mais rassurez-vous : sur la majorité des GPUs actuels, les ROPs vont à la même fréquence que le reste du GPU. ===Le ''texture fillrate'' : la performance des unités de texture=== Le '''''texture fillrate''''' est l'équivalent du ''pixel fillrate'', mais pour les textures. Pour rappel, une texture est avant tout une image, composée de pixels. Pour éviter toute confusion, ces pixels de textures sont appelés ''des texels''. Le ''texture fillrate'' est le nombre de texels que la carte graphique peut plaquer par seconde, dans le meilleur des cas. Il est mesuré en mégatexels par secondes, voire en gigatexels par secondes. L'interprétation de ce chiffre dépend de si on le mesure en entrée ou en sortie des unités de texture. En effet, les unités de texture intègrent des fonctionnalités de filtrage de texture, qui lissent les textures. Ces techniques lisent plusieurs texels et les mélangent pour fournir le texel final, celui envoyé aux unités de ''shader'' ou aux ROPs. La coutume est de le mesurer en sortie des unités de texture. Le nombre en entrée dépend grandement de la bande passante mémoire et du filtrage de texture utilisé, pas celui en sortie. Le ''texture fillrate'' en sortie est le nombre maximal d'opérations de placage de texture par seconde. Là encore, on peut l'estimer en multipliant le nombre d'unités de texture par leur fréquence. Il s'agit évidemment d'une approximation assez peu fiable, car les unités de texture peuvent mettre plusieurs cycles pour plaquer une texture, les filtrer, etc. Le ''texture fillrate'' est bien plus important que le ''pixel fillrate'', surtout pour les GPU modernes. Un point important est que le ''texture fillrate'' a longtemps été égal au ''pixel fillrate''. C'était le cas avant la Geforce 2 de NVIDIA. Les cartes graphiques avaient autant d'unités de texture que de ROP, et les deux fonctionnaient à la même fréquence. Les deux ont commencés à diverger quand le multi-texturing est arrivé, avec la Geforce 2, justement. Le nombre d'unités de texture a doublé comparé aux ROPs, ce qui fait que le ''texture fillrate'' est rapidement devenu le double du ''pixel fillrate''. Sur les GPU modernes, le ''texture fillrate'' est le triple, quadruple, voire octuple du ''pixel fillrate''. ===La performance de l'unité géométrique=== Pour l'unité géométrique, l'équivalent au ''fillrate'' est le '''''polygon throughput'''''. C'est nombre de sommets que l'unité géométrique peut traiter par seconde, exprimé en ''méga-sommets par secondes'', en millions de sommets par seconde. Il dépend de la fréquence et du nombre d'unités géométriques, mais n'est pas exactement le produit des deux. Il varie beaucoup d'une carte graphique à l'autre, mais une approximation souvent utilisée prend le quart du produit fréquence * nombre d'unités géométriques. Il faut noter que cette mesure de performance a survécu à l'arrivée des shaders. Les GPU anciens, avant DirectX 10, avaient des processeurs séparés pour les ''vertex shaders'' et les ''pixel shaders''. Mais les calculs géométriques restaient séparés des autres calculs, ils avaient des unités géométriques dédiées. Quand les processeurs de shaders dit unifiés sont arrivés, la séparation entre géométrie et autres calculs a cédé et cet indicateur a simplement disparu. ===Les autres circuits=== Pour les autres circuits, il n'y a malheureusement pas d'indicateur de performance clair et net comme peut l'être le ''fillrate''. La raison à cela se comprend assez bien quand on regarde comment se calcule le ''fillrate''. C'est juste le produit de la fréquence et d'un nombre d'unités, en l’occurrence des unités de texture ou des ROPs. Le produit signifie que ces unités travaillent en parallèle et qu'elles peuvent chacune traiter un pixel/texel indépendamment des autres. Par contre, sur les anciens GPUs de l'époque, le rastériseur et l'unité géométrique sont un seul et unique circuit. Le nombre d'unité est donc égal à 1, et il ne nous reste plus que la fréquence. {{NavChapitre | book=Les cartes graphiques | prev=Le rendu d'une scène 3D : l'API graphique | prevText=Le rendu d'une scène 3D : l'API graphique | next=Les cartes accélératrices 3D | nextText=Les cartes accélératrices 3D }} {{autocat}} 7l9mirjb53j9821ivw1iiac1m9r3phq 763455 763451 2026-04-11T15:56:09Z Mewtow 31375 /* Les autres circuits */ 763455 wikitext text/x-wiki Dans ce chapitre, nous allons voir l'architecture de base d'une carte accélératrice 3D, et voir quelle est la distinction entre une carte accélératrice et un GPU. Dans ce chapitre, nous allons faire le lien avec le rendu tel que décrit dans le chapitre précédent. Les cartes graphiques modernes implémentent des circuits programmables, qui seront partiellement laissé de côté dans ce chapitre. Nous allons aussi nous concentrer sur les cartes graphiques à placage de texture inverse, le placage de texture direct ayant déjà été abordé dans le chapitre précédent. ==L'architecture d'une carte graphique 3D== Une carte accélératrice 3D est un carte d'affichage à laquelle on aurait rajouté des circuits de rendu 3D. Elle incorpore donc tous les circuits présents sur une carte d'affichage : un VDC, une interface avec le bus, une mémoire vidéo, des circuits d’interfaçage avec l'écran, un contrôleur DMA, etc. Le VDC s'occupe de l'affichage et éventuellement du rendu 2D, mais ne s'occupe pas du traitement de la 3D. Du moins, c'est le cas sur les cartes à placage de texture inverse. Le placage de texture direct utilise au contraire un VDC avec accélération 2D très performant, comme nous l'avons vu au chapitre précédent. Mais nous mettons ce cas particulier de côté. La carte accélératrice 3D reçoit des commandes graphiques, qui proviennent du pilote de la carte graphique, exécuté sur le processeur. les commandes en question sont très variées, avec des commandes de rendu 3D, de rendu 2D, de décodage/encodage vidéo, des transferts DMA, et bien d'autres. Mais nous allons nous concentrer sur les commandes de rendu 3D, qui demandent à la carte accélératrice 3D de faire une opération de rendu 3D. Pour cela, elles précisent quel tampon de sommet utiliser, quelles textures utiliser, quels shaders sont nécessaires, etc. La carte accélératrice 3D traite ces commandes grâce à deux circuits : des circuits de rendu 3D, et un chef d'orchestre qui dirige ces circuits de rendu pour qu'ils exécutent la commande demandée. Le chef d'orchestre s'appelle le '''processeur de commandes''', et il sera vu en détail dans quelques chapitres. Pour le moment, nous allons juste dire qu'il s'occupe de la logistique, de la répartition du travail. Pour les commandes de rendu 3D, il commande les différentes étapes du pipeline graphique et s'assure que les étapes s’exécutent dans le bon ordre. [[File:Architecture globale d'une carte 3D.png|centre|vignette|upright=2|Architecture globale d'une carte 3D]] Les circuits de rendu 3D regroupent des circuits hétérogènes, aux fonctions fort différentes. Dans le cas le plus simple, il y a un circuit pour chaque étape du pipeline graphique. De tels circuits sont appelés des '''unités de traitement graphique'''. On trouve ainsi une unité pour le placage de textures, une unité de traitement de la géométrie, une unité de rasterization, une unité d'enregistrement des pixels en mémoire appelée ROP, etc. Les anciennes cartes graphiques fonctionnaient ainsi, mais on verra que les cartes graphiques modernes font un petit peu différemment. Pour simplifier les explications, nous allons séparer la carte graphique en deux gros circuits bien distincts. En réalité, ils sont souvent séparés en sous-circuits plus petits, mais laissons cela de côté pour le moment. * Les '''unités géométriques''' pour les calculs géométriques ; * Les '''pipelines de pixel''' qui rastérisent l'image, plaquent les textures, et autres. Les unités géométriques manipulent des triangles, sommets ou polygones, donc des données géométriques. Les unités de pixel font tout le reste, mais le gros de leur travail est de manipuler des pixels ou des texels. Les unités géométriques sont soit des processeurs de ''shaders'' dédiés, soit des circuits fixes (non-programmables). Leur conception a beaucoup évolué dans le temps. Les toutes premières cartes graphiques, dans les années 80 et 90, utilisaient des processeurs dédiés, programmés avec un ''firmware'' dédié. Les cartes grand public du début des années 2000 utilisaient quant à elle des circuits fixes, non-programmables. Et par la suite, les cartes ultérieures sont revenues à des processeurs, mais cette fois-ci programmables directement avec des ''shaders'' et non un ''firmware''. Les pipelines de pixels, quant à eux, ont eu une évolution bien plus simple. Avant le milieu des années 2000, elles étaient réalisées par des circuits fixes, non-programmables. Il y avait bien quelques exceptions, mais c'était la norme. Ce n'est qu'avec l'arrivée des ''pixel shaders'' que les pipelines de pixels sont devenus programmables. Ils ont alors été implémentés avec plusieurs circuits, dont un processeur de shaders et d'autres circuits non-programmables. Et il est intéressant de voir quels sont ces circuits. ===Les circuits de traitement des pixels=== Parlons un peu plus en détail des pipelines de pixels. Pour mieux comprendre ce qu'elles font, il est intéressant de regarder ce qu'il y a dans un pipeline de pixel. Un pipeline de pixel effectue plusieurs opérations les unes à la suite, dans un ordre bien précis. Et cela explique l'usage du terme "pipeline" pour les désigner. Et ces opérations sont souvent réalisées par des circuits séparés, qui sont : * Un '''rastériseur''' qui fait le lien entre triangles et pixels ; * Une '''unité de texture''' qui lit les textures et les plaque sur les modèles 3D ; * Un '''ROP''' (''Raster Operation Pipeline''), qui gère grossièrement le tampon de profondeur (''z-buffer''). Le circuit de '''rastérisation''' prend en charge la rastérisation proprement dite. Pour rappel, la rastérisation projette une scène 3D sur l'écran. Elle fait passer d'une scène 3D à un écran en 2D avec des pixels. Lors de la rastérisation, chaque sommet est associé à un ou plusieurs pixels, à savoir les pixels qu'il occupe à l'écran. Elle fournit aussi diverses informations utiles pour la suite du pipeline graphique : la profondeur du sommet associé au pixel, les coordonnées de textures qui permettent de colorier le pixel. L'étape de '''placage de texture''' lit la texture associée au modèle 3D et identifie le texel adéquat avec les coordonnées textures, pour colorier le pixel. On travaille pixel par pixel, on récupère le texel associé à chaque pixel. Soit l'inverse du placage de texture direct, qui traversait une texture texel par texel, pour recopier le texel dans le pixel adéquat. Après l'étape de placage de textures, la carte graphique enregistre le résultat en mémoire. Lors de cette étape, divers traitements de '''post-traitement''' sont effectués et divers effets peuvent être ajoutés à l'image. Un effet de brouillard peut être ajouté, des tests de profondeur sont effectués pour éliminer certains pixels cachés, l'antialiasing est ajouté, on gère les effets de transparence, etc. Un chapitre entier sera dédié à ces opérations. [[File:Unité post-géométrie d'une carte graphique sans elimination des surfaces cachées.png|centre|vignette|upright=1.5|Unité post-1.5éométrie d'une carte graphique sans elimination des surfaces cachées]] ===Les circuits d'élimination des pixels cachés=== L'élimination des surfaces cachées élimine les triangles invisibles à l'écran, car cachés par un objet opaque. En théorie, elle est prise en charge à la toute fin du pipeline, dans les ROPs, car cela permet de gérer la transparence. En effet, on ne sait pas si une texture transparente sera plaquée sur le triangle ou non. En clair, on doit éliminer les triangles invisibles après le placage de textures, et donc dans les ROP. Les ROPs se chargent à la fois de l’élimination des pixels cachées et de la transparence, les deux s’influençant l'un l'autre. [[File:Unité post-géométrie d'une carte graphique avec elimination des surfaces cachées dans les ROPs.png|centre|vignette|upright=2|Unité post-géométrie d'une carte graphique avec élimination des surfaces cachées dans les ROPs]] Il y a cependant des cas où on sait d'avance que les textures ne sont pas transparentes. Dans ce cas, la carte graphique utilise les circuits d'élimination des pixels cachés juste après la rastérisation. Cela permet d'éliminer à l'avance les triangles dont on sait qu'ils ne seront pas rendus. [[File:Unité post-géométrie d'une carte graphique.png|centre|vignette|upright=2|Unité post-géométrie d'une carte graphique]] Les deux possibilités coexistent sur les cartes graphiques modernes. Une carte graphique moderne peut éliminer les surfaces cachées avant et après la rastérisation, grâce à des techniques d''''''early-z''''' dont nous parlerons plus tard, dans un chapitre dédié sur la rastérisation. ==Les circuits d'éclairage== [[File:Implémentation de l'éclairage sur les cartes graphiques.png|vignette|Implémentation de l'éclairage sur les cartes graphiques]] Les explications précédentes décrivent une carte graphique très simple, qui ne gère pas les techniques d'éclairage. Mais elles ont disparues depuis plusieurs décennies, toutes les cartes graphiques gèrent l'éclairage en matériel depuis les années 2000. Et ces GPU des années 2000 géraient différemment l'éclairage par pixel et l'éclairage par sommet. Pour rappel, l'éclairage par sommet attribue une couleur et une luminosité à chaque sommet. L'éclairage par pixel est plus fin, car il attribue une luminosité pour chaque pixel de l'écran. Les deux étaient gérés autrefois dans des circuits distincts, comme illustré ci-contre. ===Les circuits d'éclairage par sommet=== L''''éclairage par sommet''' est grossièrement calculé dans l'unité géométrique, le circuit de calculs géométriques. L’unité de traitement géométrique peut se mettre en œuvre de deux manières. * La première utilise un circuit non-programmable, appelé le '''circuit de ''Transform & Lightning''''', qui effectue les calculs d'éclairage par sommet (d'où le L de T&L), en plus des calculs de transformation (le T de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. * Une seconde solution utilise un processeur dédié, qui exécute tous les calculs géométriques. Pour cela, il faut fournir un programme qui émule le pipeline géométrique, appelé un '''''vertex shader''''', dont nous reparlerons d'ici quelques chapitres. Intuitivement, on se dit que l'unité géométrique calcule une luminosité pour chaque triangle/sommet, comprise entre 0 (très sombre) et 1 (très brillant). Mais en réalité, l'unité de traitement géométrique calcule une couleur RGB pour chaque sommet/triangle, cette '''couleur de sommet''' indiquant quelle est sa luminosité. L'avantage est que cela simplifie la combinaison avec les textures et permet d'avoir des lumières colorées. L'unité de traitement géométrique calcul donc une couleur de sommet, qui est envoyée à l'unité de rastérisation. L'unité de rastérisation calcule la couleur du pixel à partir des trois couleurs de sommet. Pour cela, il y a deux méthodes principales, qui correspondent à l'éclairage plat et l'éclairage de Gouraud, qu'on a vu dans le chapitre précédent. La première méthode attribue la même couleur à chaque pixel d'un triangle, typiquement la moyenne des trois couleurs de sommet. La seconde méthode, celle de l'éclairage de Gouraud, calcule une couleur différente pour chaque pixel du triangle. Le calcul en question est une interpolation, à savoir une sorte de moyenne pondérée. L'éclairage de Gouraud demande donc d'ajouter un circuit d'interpolation pour les couleurs des sommets. Il fait normalement partie du circuit de rastérisation, comme on le verra plus tard dans le chapitre dédié. Pour donner un exemple, la console de jeu Playstation 1 gérait l'éclairage de Gouraud directement en matériel, mais seulement partiellement. Elle n'avait pas de circuit de T&L, ni de ''vertex shaders'', mais intégrait un circuit pour interpoler les couleurs de chaque sommet. Enfin, il faut prendre en compte les textures. Pour cela, le pixel texturé est multiplié par la luminosité/couleur calculée par l'unité géométrique. Il y a donc un '''circuit de combinaison''' situé après l'unité de texture qui effectue la combinaison/multiplication. Le circuit de combinaison est parfois configurable, à savoir qu'on peut remplacer la multiplication par une addition ou d'autres opérations. Un tel circuit de combinaison s'appelle alors un '''''combiner''''', dans la vieille nomenclature graphique de l'époque des années 90-2000. [[File:Implémentation de l'éclairage par sommet avec des combiners.png|centre|vignette|upright=2|Implémentation de l'éclairage par sommet avec des combiners]] ===Les circuits d'éclairage par pixel=== L''''éclairage par pixel''' est implémenté d'une manière totalement différente. Une implémentation naïve ajoute un circuit d'éclairage par pixel dédié, après l'unité de texture. Le circuit d’éclairage par pixel n'utilise pas la couleur de sommet, mais d'autres informations nécessaires pour calculer la luminosité d'un pixel. Il a existé quelques rares cartes graphiques capables de faire de l'éclairage de Phong en matériel. Un exemple est celui de la Geforce 3, dont l'unité géométrique implémentait des instructions dédiées pour l'algorithme de Phong. L'unité géométrique de la Geforce 3 était programmable, et elle avait une instruction Phong, qui envoyait les normales au rastériseur. Les normales étaient alors interpolées par l'unité de rastérisation, puis utilisées par une unité d'éclairage par pixel dédié, fixe, non-programmable. La technique précédente doit être adaptée pour implémenter le ''bump-mapping'' et le ''normal-mapping'', qui mémorisent des informations d'éclairage dans une texture en mémoire vidéo. La texture contient des informations de relief pour le ''bump-mapping'', des normales précalculées pour le ''normal-mapping''. Pour cela, l'unité d'éclairage par pixel doit être reliée à l'unité de texture, mais l'implémentation matérielle n'est pas aisée. Un exemple de carte graphique capable de faire cela est celle de la Nintendo DS, la PICA200. Créée par une startup japonaise, elle incorporait un circuit de T&L, un éclairage de Phong, du ''cel shading'', des techniques de ''normal-mapping'', de ''Shadow Mapping'', de ''light-mapping'', du ''cubemapping'', de nombreux effets de post-traitement (bloom, effet de flou cinétique, ''motion blur'', rendu HDR, et autres). [[File:Implémentation de l'éclairage par pixel avec des combiners.png|centre|vignette|upright=2|Implémentation de l'éclairage par pixel avec des combiners]] De nos jours, les circuits d'éclairage par pixel ont été remplacés par un '''processeur de ''pixel shader'''''. Les processeurs de ''shaders'' sont des processeurs très simples, qui exécutent des algorithmes d'éclairage par pixel appelés des ''pixel shaders''. L'avantage est que les programmeurs peuvent coder l'algorithme d'éclairage de leur choix et l'exécuter sur le GPU. Pas besoin d'avoir une unité dédiée par algorithme d'éclairage, on a un processeur de shader à tout faire. Les processeurs de shaders récupèrent les pixels émis par le rastériseur, exécutent un ''pixel shader'' dessus, puis envoient le résultat à la suite du pipeline (aux ROPs). L'unité de texture est inclue dans le processeur de ''shader'', ce qui permet au processeur de shader de lire des textures en mémoire vidéo. Le processeur de shader peut faire ce qu'il veut avec les texels lus, cela va bien au-delà d'opérations de combinaison avec une couleur de sommet. Notez que cela permet de grandement faciliter l'implémentation du ''bump-mapping'' et du ''normal-mapping''. Sur les anciens GPUs, l'unité de texture était le seul moyen pour un processeur de shader d'accéder à la mémoire vidéo, ce qui faisait que les pixels shaders pouvaient lire des textures, rien de plus. Mais de nos jours, les processeurs de shaders sont directement connectés à la mémoire vidéo et peuvent lire ou écrire dedans sans passer par l'unité de texture, ce qui peut servir pour divers algorithmes complexes. [[File:Eclairage avec des pixels shaders.png|centre|vignette|upright=2|Eclairage avec des pixels shaders]] ==Les cartes graphiques avec plusieurs unités parallèles== Plus haut, nous avons décrit une carte graphique basique, très basique, avec seulement quatre unités. Une unité pour les calculs géométriques, un rastériseur, une unité pour les pixels/textures et un ROP. Cependant, les cartes graphiques ayant cette architecture sont très rares, pour ne pas dire inexistantes. Il n'est pas impossible que les toutes premières cartes graphiques aient suivi à la lettre cette architecture, mais même cela n'est pas sur. La raison : toutes les cartes graphiques dupliquent les circuits précédents pour gagner en performance, mais aussi pour s'adapter aux contraintes du rendu 3D. ===L'amplification des pixels et son impact sur les cartes graphiques=== Un triangle prend une certaine place à l'écran, il recouvre un ou plusieurs pixels lors de l'étape de rastérisation. Le nombre de pixels recouvert dépend fortement du triangle, de sa position, de sa profondeur, etc. Un triangle peut donner quelques pixels lors de l'étape de rastérisation, alors qu'un autre va couvrir 10 fois de pixels, un autre seulement trois fois plus, un autre seulement un pixel, etc. Le cas où un triangle ne recouvre qu'un seul pixel est rare, encore que la tendance commence à changer avec les jeux vidéos récents de la décennie 2020 utilisant l'Unreal Engine et la technologie Nanite. La conséquence est qu'il y a plus de travail à faire sur les pixels que sur les sommets, ce qui a reçu le nom d''''amplification des pixels'''. La conséquence est qu'une unité géométrique prendra un triangle en entrée, l'enverra au rastériseur, qui fournira en sortie un ou plusieurs pixels à éclairer/texturer. Et cette règle un triangle = 1,N pixels fait qu'il y a un déséquilibre entre les calculs géométriques et ce qui suit, que ce soit le placage de textures, l'éclairage par pixel ou l'enregistrement des pixels dans le ''framebuffer''. Et ce déséquilibre a un impact sur la manière dont un conçoit une carte graphique, ancienne comme moderne. S'il y a une seule unité de texture/pixels, alors le rastériseur envoie chaque pixel à texturer/éclairé un par un à l'unité de pixel. Le rastériseur produits ces pixels un par un, avec un algorithme adapté pour. L'unité géométrique attendra le temps que la rastérisation ait fini de traiter tous les pixels du triangle précédent. Elle calculera le prochain triangle pendant ce temps, mais cela ne fera que limiter la casse si beaucoup de pixels sont générés. Mais il est possible de profiter de l'amplification des pixels pour gagner en performances. L'idée est que le rastériseur produit plusieurs pixels en même temps, qui sont envoyés à plusieurs unités de texture et d'éclairage par pixel. Un exemple est illustré ci-dessous, avec une seule unité géométrique, mais quatre unités de texture, quatre unités d'éclairage par pixel, et quatre ROPs. Le rastériseur est conçu pour générer quatre pixels d'un seul coup si nécessaire. [[File:Architecture d'un GPU tenant compte de l'amplification des pixels.png|centre|vignette|upright=2.5|Architecture d'un GPU tenant compte de l'amplification des pixels]] La carte graphique précédente a des performances optimales quand un triangle recouvre 4 pixels : tout est fait en une seule passe. Si un triangle ne recouvre que 1, 2 ou 3 pixels, alors le rastériseur produira 1, 2 ou 3 et certaines unités suivant le rastériseur seront inutilisées. Mais si un triangle recouvre plus de 4 pixels, alors les pixels sont générés, texturés, éclairés et enregistrés en RAM par paquets de 4. En clair, la carte graphique peut s'adapter à l'amplification des pixels, mais pas parfaitement. Les GPU récents ont résolu partiellement ce problème avec un système de ''shaders'' unifiés, mais qu'on ne peut pas expliquer pour le moment. Pour donner un exemple du monde réel, les premières cartes graphique de l'entreprise SGI était de ce type. SGI a été une entreprise pinière dans le domaine du rendu en 3D, qui a opéré dans les années 80-90, avant de progressivement décliner et fermer. Elle a conçu de nombreux systèmes de type ''workstation'', donc destinés aux professionnels, avec des cartes graphiques dédiées. le grand public n'avait pas accès à ce genre de matériel, qui était très cher, vu qu'on n'était qu'au tout début de l'informatique. Nous ne détaillerons pas ces systèmes, car ils géraient leur mémoire vidéo d'une manière assez bizarre : elle était éclatée en plusieurs morceaux fusionnés chacun avec un ROP... Mais ils avaient tous une unité géométrique unique reliée à un rastériseur, qui alimentait plusieurs unités de texture/pixel et ROPs. Plus proche de nous, certaines cartes graphiques pour PC étaient aussi dans ce cas. Les toutes premières cartes graphiques pour PC n'avaient même pas de circuits géométriques, et se contentaient d'un rastériseur, d'unités de texture et de ROPs. Par la suite, la Geforce 256 a introduit une unité géométrique appelée l'unité de T&L. Les cartes graphiques de l'époque ont suivi le mouvement et ont aussi intégrée une unité géométrique presque identique. La Geforce 256 avait une unité géométrique, mais 4 unités de texture, 4 unités d'éclairage par pixel et 4 ROPs. ===Le multitexturing : dupliquer les unités de texture=== Le '''''multi-texturing''''' est une technique très importante pour le rendu 3D moderne. L'idée est de permettre à plusieurs textures de se superposer sur un objet. Divers effets graphiques demandent d'ajouter des textures par-dessus d'autres textures, pour ajouter des détails, du relief, sur une surface pré-existante. Un exemple intéressant vient des jeux de tir : ajouter des impacts de balles sur les murs. Pour cela, on plaque une texture d'impact de balle sur le mur, à la position du tir. Il s'agit là d'un exemple de ''decals'', des petites textures ajoutées sur les murs ou le sol, afin de simuler de la poussière, des impacts de balle, des craquelures, des fissures, des trous, etc. Le ''multi-texturing'' implique que calculer un pixel implique de lire plusieurs textures. En général, un pixel avec ''multi-texturing'' demande de lire deux textures, rarement plus. La carte graphique doit alors être capable d'accéder à deux textures en même temps, ou du moins faire semblant que. De plus, elle doit combiner les deux textures pour générer le pixel voulu, ce qui demande d'ajouter un circuit qui combine deux texels (des pixels de texture) pour donner un pixel. La solution la plus simple est de doubler les unités de texture et de combiner les textures dans l'unité d'éclairage par pixel. Résultat : pour une unité d'éclairage par pixel, on a deux unités de textures. La Geforce 2 et 3 utilisaient cette solution, dont le seul défaut est que la seconde unité de texture était utilisée seulement pour les objets sur lesquels le ''multi-texturing'' était utilisé. Les cartes ATI, le concurrent de l'époque de NVIDIA, aujourd'hui racheté par AMD, triplait les unités de texture. Mais cette possibilité était peu utilisée, la majorité des jeux se dépassant pas deux texture max par pixel. C'est sans doute pour cette raison que ce triplement a été abandonné à la génération suivante, les Radeon 9000 et 8500 se contentant de doubler les unités de texture. {|class="wikitable" |- ! Nom de la carte graphique !! Unités géométriques !! Unité de texture !! Unités de pixel !! ROPs |- ! Geforce 2 d'entrée de gamme | 1 || 2 || 4 || 2 |- ! Geforce 2 milieu/haut de gamme, Geforce 3 | 1 || 4 || 8 || 4 |- ! Radeon R100 bas de gamme | 1 || 1 || 3 || 1 |- ! Radeon R100 autres | 1 || 2 || 6 || 2 |} ===L'usage de plusieurs unités géométriques=== Pour encore augmenter les performances, il est possible d'utiliser plusieurs circuits de calcul géométriques, plusieurs unités géométriques. Et ce peu importe que ces unités soient des processeurs ou des circuits fixes non-programmables. Et pour cela, il existe deux grandes implémentations : utiliser plusieurs processeurs placés en série, ou les mettre en parallèle. Comprendre la première implémentation demande de faire quelques rappels sur les calculs géométriques. ====L'usage d'un pipeline géométrique proprement dit==== Pour rappel, le pipeline géométrique regroupe les quatre étapes suivantes : * L'étape de '''chargement des sommets/triangles''', qui sont lus depuis la mémoire vidéo et injectés dans le pipeline graphique. * L'étape de '''transformation''' effectue deux changements de coordonnées pour chaque sommet. ** Premièrement, elle place les objets au bon endroit dans la scène 3D, ce qui demande de mettre à jour les coordonnées de chaque sommet de chaque modèle. C'est la première étape de calcul : l'''étape de transformation des modèles 3D''. ** Deuxièmement, elle effectue un changement de coordonnées pour centrer l'univers sur la caméra, dans la direction du regard. C'est l'étape de ''transformation de la caméra''. * La phase d''''éclairage''' (en anglais ''lighting'') attribue une couleur à chaque sommet, qui définit son niveau de luminosité : est-ce que le sommet est fortement éclairé ou est-il dans l'ombre ? * La phase d''''assemblage des primitives''' regroupe les sommets en triangles. * Les phases de '''''clipping''''' ou le '''''culling''''' agissent sur des sommets/triangles/primitives, même si elles sont souvent regroupées dans l'étape de rastérisation. Si on met de côté le chargement des sommets/triangles, il est possible de faire tous ces calculs en bloc, dans un seul processeur ou une seule unité de T&L. Mais une autre idée, plus simple, attribue un processeur/circuit pour chaque étape. En faisant cela, on peut traiter plusieurs triangles/sommets en même temps, chacun étant dans une étape différente, chacun dans un processeur/circuit. Ceux qui auront déjà lu un cours d'architecture des ordinateurs reconnaitront la fameuse technique du pipeline, mais appliquée ici à un algorithme plus conséquent. Les processeurs sont en série, et chaque processeur reçoit les résultats du processeur précédent, et envoie son résultat au processeur suivant. Sauf en début ou en bout de chaine, évidemment. Pour donner un exemple, les premières cartes graphiques de SGI utilisaient 10/12 processeurs enchainés l'un à la suite de l'autre. Les 4 premiers géraient les étapes de transformation, les 6 suivants faisaient les opérations de clipping/culling, les deux derniers faisaient la rastérisation proprement dite. Pour lisser les transferts de données, il est possible d'ajouter des mémoires FIFOs entre les processeurs. Comme ça, si un processeur est bloqué par un calcul un peu trop long, cela ne bloque pas les processeurs précédents. A la place, le processeur précédent accumule des résultats dans la mémoire FIFOs, qui seront consommé ultérieurement. En théorie, on peut s'attendre à ce que la performance soit multipliée par le nombre de processeurs. En réalité, les étapes sont rarement équilibrées, certaines étapes prennent beaucoup plus de temps que les autres, ce qui fait que la répartition des calculs n'est pas idéale : certains processeurs attendent que le processeur suivant ait finit son travail. De plus, l'organisation en pipeline entraine des couts de transmission/communication entre étapes, notamment si on utilise des mémoires FIFOs entre processeurs, ce qui est toujours le cas. Cette implémentation n'a été utilisée que sur les toutes premières cartes graphiques, avant l'apparition des PC grand public. Les systèmes SGI, utilisés pour des stations de travail, utilisaient cette architecture, par exemple. Mais elle est totalement abandonnée depuis les années 90. ====L'usage de plusieurs unités géométriques en parallèle==== La seconde solution utilise plusieurs unités géométriques en parallèle. Chaque unité géométrique traite un triangle/sommet de bout en bout, en faisant transformation, éclairage, etc. Mais vu qu'il y en a plusieurs, on peut traiter plusieurs triangles/sommets : un dans chaque unité géométrique. C'est la solution retenue sur toutes les cartes graphiques depuis les années 90. Mais la présence de plusieurs unités géométriques a deux conséquences : il faut alimenter plusieurs unités géométriques en triangles/sommets, il faut gérer l'envoi des triangles au rastériseur. Les deux demandent des solutions distinctes. La répartition du travail sur les unités géométriques est déléguée au processeur de commandes. Il utilise les unités géométriques à tour de rôle : on envoie le premier triangle à la première unité, le second triangle à la seconde unité, le troisième triangle à la troisième, etc. Il s'agit de ce que l'on appelle l''''algorithme du tourniquet''', qui est assez efficace malgré sa simplicité. Il marche assez bien quand tous les triangles/sommets mettent approximativement le même temps pour être traités. Si le temps de calcul varie beaucoup d'un triangle/sommet à l'autre, une solution toute simple détecte quels sont les processeurs de shaders libres et ceux occupés. Il suffit alors d'appliquer l'algorithme du tourniquet seulement sur les processeurs de shaders libres, qui n'ont rien à faire. Un autre problème survient cette fois-ci en sortie des unités géométriques. Comment connecter plusieurs unités géométriques au reste de la carte graphique ? Évidemment, la carte graphique contient plusieurs unités de texture/pixel et plusieurs ROPs. Elle tient compte de l'amplification des pixels, ce qui fait qu'il y a moins d'unités géométriques que d'autres circuits, entre 2 à 8 fois moins environ. Pour créer une carte graphique avec plusieurs unités géométriques, il y a plusieurs solutions, que nous allons détailler dans ce qui suit. Pour les explications, nous allons prendre l'exemple de cartes graphiques avec 2 unités géométriques et 8 unités de texture/pixel, et autant de ROPs. La première solution serait simplement de dupliquer les circuits précédents, en gardant leurs interconnexions. Pour l'exemple, on aurait 2 unités géométriques, chacune connectée à 4 unités de textures/pixels. L'unité géométrique est suivie par un rastériseur qui alimente 4 unités de texture/pixel, comme c'était le cas dans la section précédente. L'implémentation est alors très simple : on a juste à dupliquer les circuits et à modifier le processeur de commande. Il faut aussi modifier les connexions des ROPs à la mémoire vidéo. Mais les interconnexions avec le rastériseur ne sont pas modifiées. Un désavantage est que l'amplification des pixels n'est pas gérée au mieux. Imaginez que l'on ait deux triangles à rastériser, qui génèrent 8 pixels en tout : un qui génère 6 pixels à la rastérisation, l'autre seulement 2. Il n'est pas possible de traiter les 8 pixels générés. Le triangle générant deux pixels va alimenter deux unités de texture/pixels et en laisser deux inutilisées, l'autre triangle sera traité en deux fois (4 pixels, puis 2). La duplication bête et méchante n'utilise donc pas à la perfection les unités de texture/pixel. Une autre solution permet de gérer à la perfection l'amplification des pixels. Elle consiste à utiliser un seul rastériseur à haute performance, sur lequel on connecte les unités géométriques et les unités de texture/pixel. L'idée est que le rastériseur peut recevoir N triangles à la fois et alimenter M unités de texture/pixels. Le rastériseur unique s'occupe de faire plusieurs rastérisations de triangles à la fois, et répartit automatiquement les pixels générés sur les unités de texture/pixel. Pour donner un exemple, le GPU Geforce 6800 de NVIDIA avait 6 unités géométriques, 16 unités faisant à la fois placage de textures et éclairage par pixel, et 16 ROPs. Un point important avec ce GPU est qu'il n'avait qu'un seul rastériseur, détail sur lequel on reviendra dans ce qui suit ! [[File:GeForce 6800.png|centre|vignette|upright=2.5|GeForce 6800, les unités géométriques sont ici appelées les ''vertex processor'', les unités de texture/pixel sont les ''fragment processors'', les ROPs sont les ''pixel blending units''.]] ==Les cartes graphiques en mode immédiat et à tuile== Il est courant de dire qu'il existe deux types de cartes graphiques : celles en mode immédiat, et celles avec un rendu en tuiles (''tiles''). Il s'agit là des deux types principaux de cartes graphiques à l'heure actuelle, mais quelques architectures faisaient autrement dans le passé. Une autre classification, plus générale, sépare les GPU en GPU ''sort-last'', ''sort-first'' et ''sort-middle''. Les GPU en mode immédiat correspondent aux GPU en mode immédiat, alors que le rendu à tuile est une sous-catégorie des GPU ''sort-middle''. La différence entre les deux est liée à la manière dont les pixels/primitives sont répartis sur l'écran. Les GPU ''sort-first'' ont plusieurs pipelines séparés, chacun traitant une partie de l'écran. Ils déterminent la position des triangles à l'écran, puis répartissent les triangles dans les pipelines adéquats. Par exemple, on peut imaginer un GPU ''sort-first'' avec quatre unités séparées, chacune traitant un quart de l'écran. Au tout début du rendu, une unité de répartition détermine la position d'un triangle à l'écran, et l'envoie à l'unité adéquate. Si le triangle est dans le coin inférieur gauche, il sera envoyé à l'unité dédiée à ce coin. S'il est situé au milieu de l'écran, il sera envoyé aux quatre unités, chacune ne traitant les pixels que pour son coin à elle. Les GPU ''sort-middle'' découpent l'écran en carrés de 4, 8, 16, 32 pixels de côté , qui sont rendus séparément les uns des autres. Les morceaux d'image en question sont appelés des ''tiles'' en anglais, mot que nous avons décidé de ne pas traduire pour ne pas le confondre avec les tuiles du rendu 2D. Il y a une assignation stricte entre une unité de pixel/texture et une ''tile''. Par exemple, sur un système avec deux unités de texture/pixel, la première unité traitera les ''tiles'' paires, l'autre unité les ''tiles'' impaires. Les GPU ''sort-last'' sont l'extrême inverse. Ils ont des unités banalisées qui se moquent de l'endroit où se trouve un pixel à l'écran. Leurs unités géométriques traitent des polygones sans se préoccuper de leur place à l'écran. Le rastériseur envoie les pixels aux unités de textures/ROPs sans se soucier de leur place à l'écran. Encore que quelques optimisations s'en mêlent pour profiter au mieux des caches de texture et des caches intégrés aux ROPs, mais l'essentiel est qu'il n'y a pas de répartition fixe. Il n'y a pas de logique du type : ce pixel ou ce triangle est à tel endroit à l'écran, on l'envoie vers telle unité de texture/ROP. Ce sont les ROPs qui se chargent d'enregistrer les pixles finaux au bon endroits dans le ''framebuffer''. La gestion de la place des pixels à l'écran se fait donc à la toute fin du pipeline, d'où le nom de ''sort-last''. Pour résumer, les trois types de GPU se distinguent suivant l'endroit où les triangles/pixels sont répartis suivant leur place à l'écran. Avec le ''sort-first'', ce sont les triangles qui sont triés suivant leur place à l'écran. Le tri a donc lieu avant les unités géométriques. Avec le ''sort-middle'', ce sont les fragments générés par la rastérisation qui sont triés suivant leur place à l'écran, d'où l'existence de ''tiles''. Le tri a lieu entre les unités géométriques et le rastériseur. Les unités géométriques se moquent de la place à l'écran des primitives qu'ils traitent, mais pas les rastériseurs et les unités de texture. Enfin, avec le ''sort-last'', ce sont les pixels finaux qui sont triés selon leur place à l'écran, seuls les ROPs se préoccupent de cette place à l'écran. Concrètement, les GPU de type ''sort-first'' sont très rares, l'auteur de ce cours n'en connait aucun exemple. Les deux autres types de GPU sont eux beaucoup plus communs. Reste à voir ce qu'il y a à l'intérieur d'un GPU ''sort-middle'' et/ou ''sort-last''. Pour simplifier les explications, nous allons regrouper les circuits de traitement des pixels dans un seul gros circuits appelé le rastériseur, par abus de langage. La carte graphique est donc composée de deux circuits : l'unité géométrique et le mal-nommé rastériseur. Les cartes graphiques ajoutent des mémoires caches pour la géométrie et les textures, afin de rendre leur accès plus rapide. [[File:Carte graphique, généralités.png|centre|vignette|upright=2|Carte graphique, généralités]] ===Les GPU ''sort-last'', en mode immédiat=== Les cartes graphiques en mode immédiat implémentent le pipeline graphique d'une manière assez évidente. L'unité géométrique envoie des triangles au rastériseur, qui lui-même envoie les pixels à l'unité de texture, qui elle-même envoie le pixel texturé au ROP. Elles effectuent le rendu 3D triangle par tringle, pixel par pixel. Un point important est que pendant que le pixel N est dans les ROP, les pixels N+1 est dans l'unité de texture, le pixel N+2 est dans le rastériseur et le triangle suivant est dans l'unité géométrique. En clair, on n'attend pas qu'un triangle soit affiché pour en démarrer un autre. Un problème est qu'un triangle dans une scène 3D correspond souvent à plusieurs pixels, ce qui fait que la rastérisation prend plus de temps de calcul que la géométrie. En conséquence, il arrive fréquemment que le rastériseur soit occupé, alors que l'unité de géométrie veut lui envoyer des données. Pour éviter tout problème, on insère une petite mémoire entre l'unité géométrique et le rastériseur, qui porte le nom de '''tampon de primitives'''. Elle permet d'accumuler les sommets calculés quand le rastériseur est occupé. [[File:Carte graphique en rendu immédiat.png|centre|vignette|upright=2|Carte graphique en rendu immédiat]] Le tout peut s'adapter à la présence de plusieurs unités géométriques, de plusieurs unités de texture ou processeurs de shaders, tant qu'on conserve un rastériseur unique. Il suffit alors d'adapter le tampon de primitive et le rastériseur. Si on veut rajouter des unités de texture ou des processeurs de pixel shaders, le tampon de primitives n'est pas concerné : il suffit que le rastériseur ait plusieurs sorties, une par unité de texture/pixel shader. Par contre, la présence de plusieurs unités géométriques impacte le tampon de primitive. Avec plusieurs unités géométriques, il y a deux solutions : soit on garde un tampon de primitive unique partagé, soit il y a un tampon de primitive par unité géométrique. Avec la première solution, toutes les unités géométriques sont reliées à un tampon de primitives unique. Le tampon de primitive est conçu pour qu'on puisse écrire plusieurs primitives dedans en même temps. Le rastériseur n'a pas à être modifié. Une autre solution utilise un tampon de primitive par unité géométrique. Le rastériseur peut alors piocher dans plusieurs tampons de primitive, ce qui demande de modifier le rastériseur. Il y a alors un système d'arbitrage, pour que le rastériseur pioche des primitive équitablement dans tous les tampons de primitive, pas question que l'un d'entre eux soit ignoré durant trop longtemps. ===Les GPU ''sort-middle'' des années 90=== Voyons maintenant les architectures ''sort-middle'' utilisée dans les années 80-90, à une époque où les cartes graphiques grand public n'existaient pas encore. Les cartes graphiques de l’entreprise SGI sont dans ce cas, mais aussi le Pixel Planes 5, et de nombreux autres systèmes graphiques. Elles utilisaient un rendu à ''tile'' assez original. Dans ce qui suit, nous allons décrire l'architecture des systèmes SGI, qui sont représentatifs. L'idée était que l'image était découpée en un nombre de ''tiles'' qui variait selon le système utilisé, mais qui était au minimum de 5 et pouvait aller jusqu'à 20. Et chaque ''tile'' avait sa propre unité de traitement, qui contenait un rastériseur, une unité de texture, un ROP, etc. En clair, la carte graphique contenait entre 5 et 20 unités de traitement séparées, chacune dédiée à une ''tile''. Les triangles sortant des unités géométriques étaient envoyés à toutes les unités de traitement, sans exception. Une fois le triangle réceptionné, l'unité de traitement déterminait si le triangle s'affichait dans la ''tile'' associée ou non. Si c'est le cas, le rastériseur rastérise le triangle, génère les pixels, les textures sont lues, puis le tout est enregistré en mémoire vidéo. Si ce n'est pas le cas, elle abandonne le polygone/triangle reçu. Si le triangle est partiellement dans la ''tile'', le rastériseur génère les pixels qui sont dans la ''tile'', par les autres. Précisons que les cartes de ce styles incorporaient un tampon de primitive, ce qui permettait de simplifier la conception de la carte graphique. Sur la carte ''Infinite Reality'', le tampon de primitive faisait 4 méga-octets de RAM, ce permettait de mémoriser 65 536 sommets. Sur la carte ''Reality Engine'', il y avait même plusieurs tampons de primitives, un par unité géométrique. Les polygones sortaient des unités géométriques, étaient accumulés dans les tampons de primitives, puis étaient ''broadcastés'' à toutes les unités de traitement. Pour cela, le bus en bleu dans le schéma précédent est en réalité un réseau ''crossbar'' avec un système de ''broadcast''. Une caractéristique de ces architectures est qu'elles mettent le ''framebuffer'' à part de la mémoire vidéo. De plus, ce ''framebuffer'' est lui-même découpée en ''tile''. Sur la carte ''Reality Engine'', le ''framebuffer'' est découpé en 5 à 20 sous-''framebuffer'', un par ''tile''. Et chaque mini-''framebuffer'' est placé dans l'unité de traitement de la ''tile'' associée ! Ainsi, au lieu de connecter 5-20 ROPs à une mémoire vidéo unique, chaque ROP contient une '''''RAM tile''''', qui mémorise la ''tile'' en cours de traitement. Évidemment, cela pose quelques problèmes pour la connexion au VDC, en raison de l'absence de ''framebuffer'' unique, mais rien d'insurmontable. L'architecture est illustrée ci-dessous. : Le Pixel Planes 5 avait un système similaire, mais avait en plus un ''framebuffer'' complet, dans lequel les sous-''framebuffer'' étaient recopiés pour obtenir l'image finale. [[File:Architecture des premières cartes graphiques SGI.png|centre|vignette|upright=2|Architecture des premières cartes graphiques SGI]] Un autre détail de l'architecture est lié à la mémoire pour les textures. Les concepteurs de SGI ont décidé de séparer les textures dans une mémoire à part du reste de la mémoire vidéo. Il n'y a pour ainsi dire pas de mémoire vidéo proprement dit : la géométrie à rendre est dans une mémoire à part, idem pour les textures, et pour le ''framebuffer''. On s'attendrait à ce que la mémoire de texture soit reliée aux 5-20 unités de texture, mais les concepteurs ont décidé de faire autrement. A la place, chaque unité de texture contient une copie de la mémoire de texture, qui est donc dupliquée en 5-20 exemplaires ! Difficile de comprendre la raison de ce choix, mais cela simplifiait sans doute les interconnexions internes de la carte graphique, au prix d'un cout en RAM assez important. ===Les GPU à rendu à ''tile''=== Les GPU de SGI, vus précédemment, disposent de d'une unité de traitement par ''tile''. Faire ainsi permet de nombreuses optimisations, comme éclater le ''framebuffer'' en plusieurs ''RAM tile''. Mais le cout en matériel est conséquent. Pour économiser des circuits, l'idéal serait d'utiliser moins d'unités de traitement pour les pixels/fragments/textures. Mais pour cela, il faut profondément modifier l'architecture précédente. On perd forcément le lien entre une unité de traitement et une ''tile''. Et cela impose de revoir totalement la manière dont les unités géométriques communiquent avec les unités de traitement. La solution retenue est celle des GPU à rendu en ''tile'' proprement dit, aussi appelés ''GPU TBR'' (''Tile Based Rendering''). Les plus simples n'utilisent qu'une seule unité de traitement et n'ont qu'une seule ''RAM tile''. En conséquence, les ''tiles'' sont rendues l'une après l'autre. Au lieu de rendre chaque triangle/polygone l'un après l'autre, la géométrie est intégralement rendue avant de faire la rastérisation. Les triangles sont enregistrés dans la mémoire vidéo et regroupés par ''tile'', avant la rastérisation. La mémoire vidéo contient donc plusieurs paquets de triangles, avec un paquet par ''tile''. Les paquets/''tiles'' sont envoyées au rastériseur un par un, la rastérisation se fait ''tile'' par ''tile''. La ''RAM tile'' existe toujours, même si son utilité est différente. La ''RAM tile'' accélère le rendu d'une ''tile'', car tout ce qui est nécessaire pour rendre une ''tile'' est mémorisé dedans : la ''tile'', le tampon de profondeur, le tampon de stencil et plein d'autres trucs. Pas besoin d’accéder à un gigantesque z-buffer pour toute l'image, juste d'un minuscule z-buffer pour la ''tile'' en cours de traitement, qui tient totalement dans la SRAM. : Il faut noter que les ''tiles'' sont généralement assez petites : 16 ou 32 pixels de côté, rarement plus. En comparaison, les ''tiles'' faisaient 128 pixels de côté pour les cartes de SGI. [[File:Carte graphique en rendu par tiles.png|centre|vignette|upright=2|Carte graphique en rendu par tiles]] Il est possible pour un GPU TBR de traiter plusieurs ''tiles'' en même temps, en parallèle, dans des unités séparées. Un exemple est celui du GPU ARM Mali 400, qui dispose d'une unité géométrique (un processeur de ''vertex''), mais 4 processeurs de pixels. Il peut donc traiter quatre ''tiles'' en même temps, chacune étant rendue dans un processeur de pixel dédié. Les 4 processeurs de pixels ont chacun leur propre ''RAM tile'' rien qu'à eux. La présence d'une ''RAM tile'' a de nombreux avantages et impacte grandement l'architecture de la carte graphique. En premier lieu, les ROPs sont drastiquement modifiés. De nombreux GPU TBR n'ont même pas de ROPs ! A la place, les ROPs sont émulés par les processeurs de pixel shader. Les ''pixel shaders'' peuvent lire ou écrire directement dans le ''framebuffer'', sur les GPU TBR, ce qui leur permet d'émuler les ROPs avec des instructions mathématique/mémoire. Le ''driver'' patche automatiquement les ''pixel shader'' pour ajouter de quoi émuler les ROPs à la fin des ''pixel shaders''. Cela garantit une économie de circuits non-négligeable. La présence d'une ''RAM tile'' fait que le tampon de profondeur disparait. Par contre, les GPU de type TBR doivent enregistrer les triangles en mémoire vidéo, et les trier par paquets. Cela compense partiellement, totalement, ou sur-compense, les économies liée à la ''RAM tile''. Le regroupement des triangles par ''tile'' s'accompagne de quelques optimisations assez sympathiques. Par exemple, les GPU TBR modernes peuvent trier les triangles selon leur profondeur, directement lors du regroupement en paquets. L'avantage est que cela permet à l'élimination des pixels cachés de fonctionner au mieux. L'élimination des pixels cachés fonctionne à la perfection quand les triangles sont triés du plus proche au plus lointain, pour les objets opaques. Les GPU en mode immédiat ne peuvent pas faire ce tri, mais les GPU TBR peuvent le faire, soit totalement, soit partiellement. Un autre avantage est que l’antialiasing est plus rapide. Pour ceux qui ne le savent pas, l'antialiasing est une technique qui améliore la qualité d’image, en simulant une résolution supérieure. Une image rendue avec antialiasing aura la même résolution que l'écran, mais n'aura pas certains artefacts liés à une résolution insuffisante. Et l'antialiasing a lieu dans et après la rastérisation, et augmente la résolution du tampon de profondeur et du z-buffer. Les GPU en mode immédiat disposent d'optimisations pour limiter la casse, mais les ROP font malgré tout beaucoup d'accès mémoire. Avec le rendu en tiles, l'antialising se fait dans la ''RAM tile'', n'a pas besoin de passer par la mémoire vidéo et est donc plus rapide. ===Des compromis différents=== Les cartes graphiques des ordinateurs de bureau ou portables sont toutes en mode immédiat, alors que celles des appareils mobiles, smartphones et autres équipements embarqués ont un rendu en ''tiles''. Les raisons à cela sont multiples, mais la principale est que le rendu en ''tiles'' marche beaucoup mieux pour le rendu en 2D, comparé aux architectures en mode immédiat, ce qui se marie bien aux besoins des smartphones et autres objets connectés. La performance d'une carte graphique est limitée par la quantité d'accès mémoire par seconde. Autant dire que les économiser est primordial. Et les cartes en mode immédiat et par tile ne sont pas égales de ce point de vue. En mode immédiat, le tampon de primitives évite de passer par la mémoire vidéo, mais le z-buffer et le ''framebuffer'' sont très gourmand en accès mémoire. Avec les architectures à tile, c'est l'inverse : la géométrie est enregistrée en mémoire vidéo, mais le tampon de profondeur n'utilise pas la RAM vidéo. Au final, les deux architectures sont optimisées pour deux types de rendus différents. Les cartes à rendu en tile brillent quand la géométrie n'est pas trop compliquée, et que la résolution est grande ou que l'antialising est activé. Les cartes en mode immédiat sont elles douées pour les scènes géométriquement lourdes, mais avec peu d'accès aux pixels. Le tout est limité par divers caches qui tentent de rendre les accès mémoires moins fréquents, sur les deux types de cartes, mais sans que ce soit une solution miracle. ==La performance des anciennes cartes graphiques 3D== Intuitivement, la performance d'une carte graphique dépend de la performance de chacun de ses circuits : processeur de commande, mémoire vidéo, circuits de rendu 3D, VDC, etc. En pratique, il est rare qu'on soit limité par le VDC ou le processeur de commande. Les seules limitations viennent des circuits de rendu 3D et de la mémoire vidéo. Nous ne pouvons pas aborder la performance de la mémoire vidéo pour le moment. Tout ce que l'on peut dire est qu'il faut qu'elle soit assez rapide pour alimenter le rendu 3D en données. Les circuits de rendu 3D doivent lire des triangles et textures en mémoire vidéo, qui doit être assez rapide pour ça et ne pas les faire attendre. Pour le reste, voyons la performance des circuits de rendu 3D. Il ne nous est là aussi pas possible de détailler ce qui impacte la performance d'un GPU moderne. Dès que des processeurs de shaders sont impliqués, parler de performance demande de connaitre sur le bout des doigts les processeurs de shaders, ce qu'on n'a pas encore vu à ce stade du cours. Par contre, on peut détailler ce qu'il en était pour les anciennes cartes 3D, sans processeurs de shaders. Elles contenaient des ROPs, des unités de texture, un rastériseur et une unité géométrique (l'unité de T&L). Étudions d'abord la performance des unités de texture et des ROPs. Cela nous permettra de parler d'un paramètre qui avait son importance sur les anciennes cartes graphiques, avant les années 2000 : le ''fillrate''. Le '''''fill rate''''', ou taux de remplissage, est une ancienne mesure de performance autrefois utilisée pour comparer les cartes graphiques entre elles. Il s'agit d'une mesure assez approximative, au même titre que la fréquence d'horloge. Concrètement, plus il est élevé, meilleures seront les performances, en théorie. Mais attention : les petites différences de ''fillrate'' ne suffisent pas à rendre un verdict. De plus, il existe deux types distincts de ''fillrate'' : le ''Texture Fillrate'' et le ''Pixel Fillrate''. Voyons d'abord le ''Pixel Fillrate''. ===Le ''pixel fillrate'' : la performance des ROPs=== Le '''''pixel fillrate''''' est le nombre maximal de pixels que la carte graphique peut écrire en mémoire vidéo par seconde. Il est exprimé en ''Méga-Pixels par seconde'' ou en ''Giga-Pixels par seconde'', souvent abréviés en GP/s et MP/s. C'est une unité que vous croisez sans doute pour la première fois et qui mérite quelques explications. Premièrement, dans méga-pixels par seconde, il y a mégapixels. Il s'agit d'une unité pour compter le nombre de pixels d'une image. Un mégapixel signifie tout simplement un million de pixels, un gigapixel signifie un milliard de pixels. Je précise bien un million et un milliard, ce ne sont pas des multiples de 1024, comme on est habitué à en voir en informatique. Le nombre de pixels d'une image augmente avec la résolution utilisée, mais il reste de l'ordre du mégapixel, guère plus. Voici un tableau avec les résolutions les plus utilisées et le nombre de pixels associé. {|class="wikitable" |- ! Résolution !! Nombre de pixels |- | colspan="2" | |- | colspan="2" | Résolutions anciennes en 4:3 |- | 640 × 480 || 307 200 <math>\approx</math> 0,3 MP |- | 800 × 600 || 480 000 = 0,48 MP |- | 1 024 × 768 || 786 432 <math>\approx</math> 0,8 MP |- | 1 280 × 960 || 1 228 800 <math>\approx</math> 1,2 MP |- | 1 600 × 1 200 || 1 920 000 = 1,92 MP |- | colspan="2" | |- | colspan="2" | Résolutions modernes en 16:9 |- | 1 920 × 1 080 || 2 073 600 <math>\approx</math> 2 MP |- | 3 840 × 2 160 (4k) || 8 294 400 <math>\approx</math> 8.3 MP |} Maintenant, regardons ce qui se passe si on veut rendre plusieurs images par secondes. Intuitivement, on se dit qu'il faudra un ''pixel fillrate'' minimal pour cela. Et il se trouve qu'on peut le calculer aisément. Prenons par exemple une image en 1600 × 1200, de 1,92 mégapixels. Si on veut avoir 60 images par secondes, avec cette résolution, cela fait 1,92 * 60 mégapixels par secondes. En clair, le ''pixel fillrate'' minimal se calcule en multipliant la résolution par le ''framerate''. Le ''pixel fillrate'' minimal tourne autour de la centaine de mégapixels par seconde, voire approche le gigapixel par seconde en haute résolution. Les images font entre 1 et 10 mégapixels, pour environ 100 FPS, l'intervalle colle parfaitement. Maintenant, comparons un peu avec ce dont sont capables les GPUs. Les toutes premières cartes graphiques commerciales avaient un ''pixel fillrate'' proche de la centaine de méga-pixels par seconde. Pour donner un exemple, la Geforce 256 avait un ''pixel fillrate'' de 480 MP/s, la Geforce 3 faisait entre 700 et 960 MP/s selon le modèle. De nos jours, le ''pixel fillrate'' est de l'ordre de la centaine de Gigapixels. Pour donner un exemple, les Geforce RTX 5000 ont un ''pixel fillrate'' de 82.3GP/s pour la RTX 5050, à 423.6 GP/S pour la RTX 5090. Les GPU ont un ''pixel fillrate'' qui dépasse de très loin la valeur minimale, ce qui est franchement étrange. La raison à cela est que le ''pixel fillrate'' minimal se calcule sous l'hypothèse que chaque pixel de l'image finale ne sera écrit qu'une seule fois. Mais dans les faits, il est fréquent qu'un pixel soit dessiné plusieurs fois avant d'obtenir l'image finale. La raison principale est liée aux surfaces cachées. Si un objet est derrière un autre, il arrive que celui-ci soit dessiné dans le ''framebuffer'', avant que l'objet devant soit re-dessiné par-dessus. Des pixels ont alors été écrits, puis ré-écrits. Le fait de dessiner un pixel plusieurs fois porte un nom. Il s'agit d'un phénomène d''''''overdraw''''', ou sur-dessinage en français. Le sur-dessinage fait que le ''pixel fillrate'' minimal ne suffit pas en pratique. Pour éviter tout problème, le ''pixel fillrate'' du GPU doit être supérieur au ''pixel fillrate'' minimal, d'environ un ordre de grandeur. L'élimination des surfaces cachées réduit l'''overdraw'', mais elle ne fait pas de miracles. En pratique, le sur-dessinage ne concerne qu'une partie assez mineure des pixels de l'image, et un pixel est rarement écrit plus d'une dizaine de fois. Et les GPus modernes ont un ''pixel fillrate'' tellement démentiel qu'il n'est presque jamais un facteur limitant. Le ''pixel fillrate'' d'un GPU dépend de plusieurs choses : le nombre de ROPs, leur fréquence d'horloge exprimée en MHz/GHz, la bande passante mémoire, et bien d'autres. En théorie, la bande passante mémoire n'est pas un point limitant, les concepteurs du GPU prévoient une mémoire suffisamment rapide pour qu'elle puisse encaisser le ''pixel fillrate'' maximal, tout en ayant encore de la marge pour lire des textures et la géométrie. En clair, le ''pixel fillrate'' est surtout dépendant des ROPs, de leur nombre, de leur vitesse, de leur implémentation. Le ''pixel fillrate'' du GPU est difficile à calculer, mais l'approximation la plus utilisée est la suivante. Elle part du principe qu'un ROP peut écrire un pixel par cycle d'horloge. Ce n'est pas forcément le cas, tout dépend de l'implémentation des ROPs. Certains GPU performants ont des ROPs capables d'écrire des blocs de 8*8 pixels d'un seul coup en mémoire vidéo, alors que d'anciens GPU font avec des ROPs limités, seulement capables d'écrire un pixel tout les 10 cycles d'horloge. Toujours est-il qu'avec cette hypothèse, le ''pixel fillrate'' est égal au nombre de ROPs, multiplié par leur fréquence d'horloge. Je précise "leur" fréquence d'horloge, car il est possible de faire fonctionner l'unité de T&L, les ROPs, les unités de texture et le rastériseur à des fréquences différentes. C'est parfaitement possible, le cout en performance est parfois assez faible, mais le gain en consommation d'énergie est souvent important. Et justement, il a existé des GPU sur lesquels les ROPs avaient une fréquence inférieure à celle du reste du GPU. Dans ce cas, c'est la fréquence des ROPs qui est importante. Mais rassurez-vous : sur la majorité des GPUs actuels, les ROPs vont à la même fréquence que le reste du GPU. ===Le ''texture fillrate'' : la performance des unités de texture=== Le '''''texture fillrate''''' est l'équivalent du ''pixel fillrate'', mais pour les textures. Pour rappel, une texture est avant tout une image, composée de pixels. Pour éviter toute confusion, ces pixels de textures sont appelés ''des texels''. Le ''texture fillrate'' est le nombre de texels que la carte graphique peut plaquer par seconde, dans le meilleur des cas. Il est mesuré en mégatexels par secondes, voire en gigatexels par secondes. L'interprétation de ce chiffre dépend de si on le mesure en entrée ou en sortie des unités de texture. En effet, les unités de texture intègrent des fonctionnalités de filtrage de texture, qui lissent les textures. Ces techniques lisent plusieurs texels et les mélangent pour fournir le texel final, celui envoyé aux unités de ''shader'' ou aux ROPs. La coutume est de le mesurer en sortie des unités de texture. Le nombre en entrée dépend grandement de la bande passante mémoire et du filtrage de texture utilisé, pas celui en sortie. Le ''texture fillrate'' en sortie est le nombre maximal d'opérations de placage de texture par seconde. Là encore, on peut l'estimer en multipliant le nombre d'unités de texture par leur fréquence. Il s'agit évidemment d'une approximation assez peu fiable, car les unités de texture peuvent mettre plusieurs cycles pour plaquer une texture, les filtrer, etc. Le ''texture fillrate'' est bien plus important que le ''pixel fillrate'', surtout pour les GPU modernes. Un point important est que le ''texture fillrate'' a longtemps été égal au ''pixel fillrate''. C'était le cas avant la Geforce 2 de NVIDIA. Les cartes graphiques avaient autant d'unités de texture que de ROP, et les deux fonctionnaient à la même fréquence. Les deux ont commencés à diverger quand le multi-texturing est arrivé, avec la Geforce 2, justement. Le nombre d'unités de texture a doublé comparé aux ROPs, ce qui fait que le ''texture fillrate'' est rapidement devenu le double du ''pixel fillrate''. Sur les GPU modernes, le ''texture fillrate'' est le triple, quadruple, voire octuple du ''pixel fillrate''. ===La performance de l'unité géométrique=== Pour l'unité géométrique, l'équivalent au ''fillrate'' est le '''''polygon throughput'''''. C'est nombre de sommets que l'unité géométrique peut traiter par seconde, exprimé en ''méga-sommets par secondes'', en millions de sommets par seconde. Il dépend de la fréquence et du nombre d'unités géométriques, mais n'est pas exactement le produit des deux. Il varie beaucoup d'une carte graphique à l'autre, mais une approximation souvent utilisée prend le quart du produit fréquence * nombre d'unités géométriques. Il faut noter que cette mesure de performance a survécu à l'arrivée des shaders. Les GPU anciens, avant DirectX 10, avaient des processeurs séparés pour les ''vertex shaders'' et les ''pixel shaders''. Mais les calculs géométriques restaient séparés des autres calculs, ils avaient des unités géométriques dédiées. Quand les processeurs de shaders dit unifiés sont arrivés, la séparation entre géométrie et autres calculs a cédé et cet indicateur a simplement disparu. ===Les autres circuits=== Pour les autres circuits, il n'y a malheureusement pas d'indicateur de performance clair et net comme peut l'être le ''fillrate''. La raison à cela se comprend assez bien quand on regarde comment se calcule le ''fillrate''. C'est juste le produit de la fréquence et d'un nombre d'unités, en l’occurrence des unités de texture ou des ROPs. Le produit signifie que ces unités travaillent en parallèle et qu'elles peuvent chacune traiter un pixel/texel indépendamment des autres. Par contre, sur les anciens GPUs de l'époque, le rastériseur et l'unité géométrique sont un seul et unique circuit. Le nombre d'unité est donc égal à 1, et il ne nous reste plus que la fréquence. {{NavChapitre | book=Les cartes graphiques | prev=Le rendu d'une scène 3D : concepts de base | prevText=Le rendu d'une scène 3D : concepts de base | next=Les cartes accélératrices 3D | nextText=Les cartes accélératrices 3D }} {{autocat}} ju39xin9mk0qjt1lap314a7l9ibz9mi Essai pour un modèle de psychisme objectif/cytologie et communication humaine 0 83703 763512 761857 2026-04-11T23:11:00Z Litlok 2308 quelque soit → quelle que soit 763512 wikitext text/x-wiki     Digression à propos de la taille des cellules vivantes. Quelle que soit la taille de l'organisme vivant, la taille de ses cellules reste petite. C'est une nécessité physiologique fondamentale puisque l'activité cellulaire est tributaire de l'échange avec le milieu extérieur. Si l'on admet que le travail possible par une cellule est fonction de son volume, et qu'il entraîne un besoin proportionnel d'échange, on voit que, à environnement constant, le flux d'échange pour chaque unité de volume décroît dans une proportion 3:R (3 divisé par le rayon). Ainsi si l'objectif est de produire un grand travail, la seule solution est d'obtenir la collaboration d'un grand nombre de cellules. Nous comprenons que la communication cellulaire devient cruciale pour l'efficacité et, pour faire court, que l'extension du système nerveux humain est une conséquence directe de cette donnée physiologique. L’analogie avec la société humaine est très claire.     Ainsi la communication interhumaine, et donc une compréhension linguistique et psychique efficace, sont des déterminants essentiels pour l’existence et la survie de l’humanité. {{AutoCat}} mjj5m7r26m01y8tpo7946hnjpgfc32z La grammaire fondamentale de l'ido/Mots grammaticaux/Numéraux 0 83743 763514 763312 2026-04-12T08:06:29Z Francucelo 123176 763514 wikitext text/x-wiki L'ido a treize numéraux : {| class="wikitable" !Chiffre !Numéral |- |0 |zero |- |1 |un |- |2 |du |- |3 |tri |- |4 |quar |- |5 |kin |- |6 |sis |- |7 |sep |- |8 |ok |- |9 |non |- |10 |dek |- |100 |cent |- |1000 |mil |} == Exprimer les nombres == Pour exprimer un nombre, on combine les numéraux. L'ordre d'énonciation va de la valeur la plus élevée à la plus faible : on commence par les milliers, puis les centaines, ensuite les dizaines et enfin les unités. Si une position ne contient aucun chiffre (c'est-à-dire si elle est nulle), on ne la mentionne pas. Chaque position est représentée par un mot, issu de la combinaison des mots qui désignent les différentes valeurs. Pour former des nombres composés de deux chiffres ou plus, on suit la règle suivante : on relie les chiffres (du plus petit au plus grand) par la lettre « a ». * Par exemple, 30 000 se dit « Tri-a-dek-a-mil », ce qui signifie littéralement « trois fois dix mille ». On écrit généralement « Triadekamil » sans trait d'union. De plus, pour la phonétique, il faut ajouter un « -e- » entre le chiffre des dizaines et celui des unités (lorsqu'il y en a). Ce lien fait que le chiffre des dizaines et celui des unités forment un seul mot. L'accent tonique reste alors sur l'avant-dernière syllabe. * Par exemple, 42 se dit « Quaradek-e-du », l'accent tonique étant sur le « e ». Pour exprimer un nombre décimal, on lit d'abord la partie entière, puis on ajoute une « komo » (virgule) avant de lire les décimales une à une. * Par exemple, 3,14 se dit « Tri komo un quar ». {| class="wikitable" |+Chiffres concrets !Chiffre !Ido |- |11 |dek-e-un |- |12 |dek-e-du |- |13 |dek-e-tri |- |14 |dek-e-quar |- |15 |dek-e-kin |- |16 |dek-e-ses |- |17 |dek-e-sep |- |18 |dek-e-ok |- |19 |dek-e-non |- |20 |duadek |- |30 |triadek |- |40 |quaradek |- |50 |kinadek |- |60 |sesadek |- |70 |sepadek |- |80 |okadek |- |90 |nonadek |- |200 |duacent |- |404 |quaracent quar |- |2000 |duacent |- |2048 |duacent quaradek-e-ok |- |10,000 |dekamil |- |20,000 |duadekamil |- |100,000 |centamil |- |200,000 |duacentamil |- |999,999 |nonacentamil nonadekamil nonamil nonacent nonadek-e-non |} == Exprimer la quantité == Les nombres sont placés devant le nom pour en indiquer la quantité. Le nom doit changer de terminaison (singulier ou pluriel) en fonction du nombre (voir la section « [[La grammaire fondamentale de l'ido/Mots lexicaux|Mots]] [[La grammaire fondamentale de l'ido/Mots lexicaux|lexicaux : Noms]] »). * Par exemple « la tri kati » (les trois chats). == Expression de la séquence == Pour exprimer un ordre, on peut placer le nombre après le groupe nominal ou transformer le nombre en adjectif. Pour transformer un nombre en adjectif ordinal, il faut ajouter le suffixe « esm » après le nombre et terminer par la terminaison « a ». * Par exemple « chapitro quaradek-e-du » (chapitre quarante-deux) et « quaradek-e-duesma chapitro » (quarante-deuxième chapitre) == Exercices == Essayez de formuler les phrases suivantes, les racines nécessaires sont indiquées ci-dessous. * 2,718 * 867 documents ** Document : Dokumentar * 101e personne ** Personne : Person {{Boîte déroulante|titre=Voir les réponses|contenu=* Du komo sep un ok. * Okacent sesadek-e-sep dokumentari. * Cent unesma persono. ** Ou : Persono cent un.}} {{AutoCat}} ls4b0tgjsvr0i4dael3iemolttrs226 763515 763514 2026-04-12T08:22:03Z Francucelo 123176 763515 wikitext text/x-wiki L'ido a treize numéraux : {| class="wikitable" !Chiffre !Numéral |- |0 |zero |- |1 |un |- |2 |du |- |3 |tri |- |4 |quar |- |5 |kin |- |6 |sis |- |7 |sep |- |8 |ok |- |9 |non |- |10 |dek |- |100 |cent |- |1000 |mil |} == Exprimer les nombres == Pour exprimer un nombre, on combine les numéraux. L'ordre d'énonciation va de la valeur la plus élevée à la plus faible : on commence par les milliers, puis les centaines, ensuite les dizaines et enfin les unités. Si une position ne contient aucun chiffre (c'est-à-dire si elle est nulle), on ne la mentionne pas. Chaque position est représentée par un mot, issu de la combinaison des mots qui désignent les différentes valeurs. Pour former des nombres composés de deux chiffres ou plus, on suit la règle suivante : on relie les chiffres (du plus petit au plus grand) par la lettre « a ». * Par exemple, 30 000 se dit « Tri-a-dek-a-mil », ce qui signifie littéralement « trois fois dix mille ». On écrit généralement « Triadekamil » sans trait d'union. Pour relier des chiffres différents, il faut utiliser le trait d'union « e », qui indique une addition.. * Par exemple, 42 se dit « Quaradek e du ». Pour exprimer un nombre décimal, on lit d'abord la partie entière, puis on ajoute une « komo » (virgule) avant de lire les décimales une à une. * Par exemple, 3,14 se dit « Tri komo un quar ». {| class="wikitable" |+Chiffres concrets !Chiffre !Ido |- |11 |dek e un |- |20 |duadek |- |30 |triadek |- |40 |quaradek |- |50 |kinadek |- |60 |sisadek |- |70 |sepadek |- |80 |okadek |- |90 |nonadek |- |200 |duacent |- |404 |quaracent e quar |- |2000 |duacent |- |2048 |duacent e quaradek e ok |- |10,000 |dekamil |- |20,000 |duadekamil |- |100,000 |centamil |- |200,000 |duacentamil |- |999,999 |nonacentamil e nonadekamil e nonamil e nonacent e nonadek e non |} == Exprimer la quantité == Les nombres sont placés devant le nom pour en indiquer la quantité. Le nom doit changer de terminaison (singulier ou pluriel) en fonction du nombre (voir la section « [[La grammaire fondamentale de l'ido/Mots lexicaux|Mots]] [[La grammaire fondamentale de l'ido/Mots lexicaux|lexicaux : Noms]] »). * Par exemple « la tri kati » (les trois chats). == Expression de la séquence == Pour exprimer un ordre, on peut placer le nombre après le groupe nominal ou transformer le nombre en adjectif. Pour transformer un nombre en adjectif ordinal, il faut ajouter le suffixe « esm » après le nombre et terminer par la terminaison « a ». * Par exemple « chapitro quaradek e du » (chapitre quarante-deux) et « quaradek e duesma chapitro » (quarante-deuxième chapitre) == Exercices == Essayez de formuler les phrases suivantes, les racines nécessaires sont indiquées ci-dessous. * 2,718 * 867 documents ** Document : Dokumentar * 101e personne ** Personne : Person {{Boîte déroulante|titre=Voir les réponses|contenu=* Du komo sep un ok. * Okacent e sisadek e sep dokumentari. * Cent e unesma persono. ** Ou : Persono cent e un.}} {{AutoCat}} 8flbfty5corqbn1f9gilqz0quqcgbwo 763516 763515 2026-04-12T08:27:01Z Francucelo 123176 /* Exprimer les nombres */ 763516 wikitext text/x-wiki L'ido a treize numéraux : {| class="wikitable" !Chiffre !Numéral |- |0 |zero |- |1 |un |- |2 |du |- |3 |tri |- |4 |quar |- |5 |kin |- |6 |sis |- |7 |sep |- |8 |ok |- |9 |non |- |10 |dek |- |100 |cent |- |1000 |mil |} == Exprimer les nombres == Pour exprimer un nombre, on combine les numéraux. L'ordre d'énonciation va de la valeur la plus élevée à la plus faible : on commence par les milliers, puis les centaines, ensuite les dizaines et enfin les unités. Si une position ne contient aucun chiffre (c'est-à-dire si elle est nulle), on ne la mentionne pas. Chaque position est représentée par un mot, issu de la combinaison des mots qui désignent les différentes valeurs. Pour former des nombres composés de deux chiffres ou plus, on suit la règle suivante : on relie les chiffres (du plus petit au plus grand) par la lettre « a ». * Par exemple, 30 000 se dit « Tri-a-dek-a-mil », ce qui signifie littéralement « trois fois dix mille ». On écrit généralement « Triadekamil » sans trait d'union. Pour relier des chiffres différents, il faut utiliser la conjonction « e », qui indique une addition. * Par exemple, 42 se dit « Quaradek e du ». Pour exprimer un nombre décimal, on lit d'abord la partie entière, puis on ajoute une « komo » (virgule) avant de lire les décimales une à une. * Par exemple, 3,14 se dit « Tri komo un quar ». {| class="wikitable" |+Chiffres concrets !Chiffre !Ido |- |11 |dek e un |- |20 |duadek |- |30 |triadek |- |40 |quaradek |- |50 |kinadek |- |60 |sisadek |- |70 |sepadek |- |80 |okadek |- |90 |nonadek |- |200 |duacent |- |404 |quaracent e quar |- |2000 |duacent |- |2048 |duacent e quaradek e ok |- |10,000 |dekamil |- |20,000 |duadekamil |- |100,000 |centamil |- |200,000 |duacentamil |- |999,999 |nonacentamil e nonadekamil e nonamil e nonacent e nonadek e non |} == Exprimer la quantité == Les nombres sont placés devant le nom pour en indiquer la quantité. Le nom doit changer de terminaison (singulier ou pluriel) en fonction du nombre (voir la section « [[La grammaire fondamentale de l'ido/Mots lexicaux|Mots]] [[La grammaire fondamentale de l'ido/Mots lexicaux|lexicaux : Noms]] »). * Par exemple « la tri kati » (les trois chats). == Expression de la séquence == Pour exprimer un ordre, on peut placer le nombre après le groupe nominal ou transformer le nombre en adjectif. Pour transformer un nombre en adjectif ordinal, il faut ajouter le suffixe « esm » après le nombre et terminer par la terminaison « a ». * Par exemple « chapitro quaradek e du » (chapitre quarante-deux) et « quaradek e duesma chapitro » (quarante-deuxième chapitre) == Exercices == Essayez de formuler les phrases suivantes, les racines nécessaires sont indiquées ci-dessous. * 2,718 * 867 documents ** Document : Dokumentar * 101e personne ** Personne : Person {{Boîte déroulante|titre=Voir les réponses|contenu=* Du komo sep un ok. * Okacent e sisadek e sep dokumentari. * Cent e unesma persono. ** Ou : Persono cent e un.}} {{AutoCat}} qmt4lybvh86miooprtgv7miuivju86y La grammaire fondamentale de l'ido/Affixes 0 83764 763452 763355 2026-04-11T15:45:17Z Francucelo 123176 /* Principaux suffixes */ 763452 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 |- |ACH |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}} j4ic9km62s392zdcfhy8opi022phw5u 763453 763452 2026-04-11T15:45:29Z Francucelo 123176 /* Principaux suffixes */ 763453 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 |- |ACH |De mauvaise qualité, en piteux état |Dom-ach-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}} bwr2efsvpfpc2qzndh0s7i54jj9g3km Les cartes graphiques/Le pipeline géométrique d'avant DirectX 10 0 83791 763458 2026-04-11T15:59:42Z Mewtow 31375 Mewtow a déplacé la page [[Les cartes graphiques/Le pipeline géométrique d'avant DirectX 10]] vers [[Les cartes graphiques/Le pipeline géométrique : évolution]] 763458 wikitext text/x-wiki #REDIRECTION [[Les cartes graphiques/Le pipeline géométrique : évolution]] pquowhpzwsnobkt2dwtc4plvodwi9ee Les cartes graphiques/Le pipeline géométrique après DirectX 10 0 83792 763462 2026-04-11T16:01:03Z Mewtow 31375 Mewtow a déplacé la page [[Les cartes graphiques/Le pipeline géométrique après DirectX 10]] vers [[Les cartes graphiques/Le pipeline géométrique d'un GPU]] 763462 wikitext text/x-wiki #REDIRECTION [[Les cartes graphiques/Le pipeline géométrique d'un GPU]] tjjmlifbeue8rjp9pf2mttiabg4nzea