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
Latin/Vocabulaire/Verbes irréguliers et défectifs
0
6981
763435
489228
2026-04-11T09:30:30Z
Chesspac
110086
763435
wikitext
text/x-wiki
==Verbes irréguliers==
=== ===
*volo, vis, velle:vouloir
*nolo, non vis, nolle:ne pas vouloir
*malo, mavis, malle:préférer
=== esse et ses dérivés ===
*'''sum''', es, esse : être
*absum, abes, abesse : être absent, être loin
*adsum, ades, adesse : être présent
*desum, dees, deesse : manquer à
*insum, ines, inesse : être dans
*intersum, interes, interesse : être entre
*obsum, obes, obesse : nuire à
*possum, poses, posse : pouvoir
*praesum, praees, praeesse : être à la tête de
*prosum, prodes, prodesse : être utile à
*subsum, subes, subesse : être sous
*supersum, superes, superesse : survivre à
=== ferre et ses dérivés ===
*'''ferre''', fero, fers, tuli, latum : porter
*adferre, adfero, attuli, allatum : apporter
*auferre, aufero, abstuli, ablatum : emporter, enlever
*conferre, confero, contuli, collatum
*offerre, offero, obtuli, oblatum : offrir
*referre, refero, rettuli, relatum : rapporter
*sufferre, suffero, sustuli, sublatum : supporter
=== ire et ses dérivés ===
*'''ire''', eo, is, ii, itum : aller
*abire, abeo, abii, abitum : s'en aller, s'éloigner
*adire, adeo, adii, aditum : aller vers
*exire, exeo, exii, exitum : sortir
*perire, pereo, perii, peritum : périr, disparaître
*redire, redeo, redii, reditum : revenir
*subire, subeo, subii, subitum : aller sous; s'avancer; envahir; subir
*transire, transeo, transii, transitum : traverser
== Verbes défectifs courants ==
*aio, ait : je dis, il dit
*coepisse : commencer
*inquam, inquit : dis-je, dit-il
*meminisse : se souvenir
*noui : je sais
*odisse : haïr
{{Latin/Vocabulaire}}
[[Catégorie:Latin (livre)]]
[[Catégorie:Verbes|latin]]
inp4f3jsoo5fpo2j91v0q762pqvyk4w
763436
763435
2026-04-11T09:33:55Z
Chesspac
110086
/* Verbes irréguliers */
763436
wikitext
text/x-wiki
==Verbes irréguliers==
=== velle et ses dérivés ===
*'''volo''', vis, velle : vouloir
*nolo, non vis, nolle : ne pas vouloir
*malo, mavis, malle : préférer
=== esse et ses dérivés ===
*'''sum''', es, esse : être
*absum, abes, abesse : être absent, être loin
*adsum, ades, adesse : être présent
*desum, dees, deesse : manquer à
*insum, ines, inesse : être dans
*intersum, interes, interesse : être entre
*obsum, obes, obesse : nuire à
*possum, poses, posse : pouvoir
*praesum, praees, praeesse : être à la tête de
*prosum, prodes, prodesse : être utile à
*subsum, subes, subesse : être sous
*supersum, superes, superesse : survivre à
=== ferre et ses dérivés ===
*'''ferre''', fero, fers, tuli, latum : porter
*adferre, adfero, attuli, allatum : apporter
*auferre, aufero, abstuli, ablatum : emporter, enlever
*conferre, confero, contuli, collatum
*offerre, offero, obtuli, oblatum : offrir
*referre, refero, rettuli, relatum : rapporter
*sufferre, suffero, sustuli, sublatum : supporter
=== ire et ses dérivés ===
*'''ire''', eo, is, ii, itum : aller
*abire, abeo, abii, abitum : s'en aller, s'éloigner
*adire, adeo, adii, aditum : aller vers
*exire, exeo, exii, exitum : sortir
*perire, pereo, perii, peritum : périr, disparaître
*redire, redeo, redii, reditum : revenir
*subire, subeo, subii, subitum : aller sous; s'avancer; envahir; subir
*transire, transeo, transii, transitum : traverser
=== edere ===
*'''edo''' : conjugaison régulière existante, mais les formes irrégulières suivantes sont souvent utilisées :
** Infinitif : esse
** 2e PS présent : es, 3e PS présent : est, 2e PP présent : estis
** Radical imparfait subjonctif : essem
** Impératif : es, este
== Verbes défectifs courants ==
*aio, ait : je dis, il dit
*coepisse : commencer
*inquam, inquit : dis-je, dit-il
*meminisse : se souvenir
*noui : je sais
*odisse : haïr
{{Latin/Vocabulaire}}
[[Catégorie:Latin (livre)]]
[[Catégorie:Verbes|latin]]
kccuuiaxjp8r7qefleurj9jqpix5wjr
763437
763436
2026-04-11T09:37:54Z
Chesspac
110086
/* edere */
763437
wikitext
text/x-wiki
==Verbes irréguliers==
=== velle et ses dérivés ===
*'''volo''', vis, velle : vouloir
*nolo, non vis, nolle : ne pas vouloir
*malo, mavis, malle : préférer
=== esse et ses dérivés ===
*'''sum''', es, esse : être
*absum, abes, abesse : être absent, être loin
*adsum, ades, adesse : être présent
*desum, dees, deesse : manquer à
*insum, ines, inesse : être dans
*intersum, interes, interesse : être entre
*obsum, obes, obesse : nuire à
*possum, poses, posse : pouvoir
*praesum, praees, praeesse : être à la tête de
*prosum, prodes, prodesse : être utile à
*subsum, subes, subesse : être sous
*supersum, superes, superesse : survivre à
=== ferre et ses dérivés ===
*'''ferre''', fero, fers, tuli, latum : porter
*adferre, adfero, attuli, allatum : apporter
*auferre, aufero, abstuli, ablatum : emporter, enlever
*conferre, confero, contuli, collatum
*offerre, offero, obtuli, oblatum : offrir
*referre, refero, rettuli, relatum : rapporter
*sufferre, suffero, sustuli, sublatum : supporter
=== ire et ses dérivés ===
*'''ire''', eo, is, ii, itum : aller
*abire, abeo, abii, abitum : s'en aller, s'éloigner
*adire, adeo, adii, aditum : aller vers
*exire, exeo, exii, exitum : sortir
*perire, pereo, perii, peritum : périr, disparaître
*redire, redeo, redii, reditum : revenir
*subire, subeo, subii, subitum : aller sous; s'avancer; envahir; subir
*transire, transeo, transii, transitum : traverser
=== fieri et ses composés ===
* '''fio''', fis, fieri : devenir
* confio, confis, confieri : avoir lieu, se produire ; être épuisé
* defit, defieri : il manque à
* infit, infieri : ça commence à, il commence à
* interfio, interfis, interfieri : être détruit
* malefio, malefis, malefieri : se faire du mal, être blessé
* superfio, superfis, superfieri : être de reste, rester
=== edere ===
*'''edo''' : conjugaison régulière existante, mais les formes irrégulières suivantes sont souvent utilisées :
** Infinitif : esse
** 2e PS présent : es, 3e PS présent : est, 2e PP présent : estis
** Radical imparfait subjonctif : essem
** Impératif : es, este
== Verbes défectifs courants ==
*aio, ait : je dis, il dit
*coepisse : commencer
*inquam, inquit : dis-je, dit-il
*meminisse : se souvenir
*noui : je sais
*odisse : haïr
{{Latin/Vocabulaire}}
[[Catégorie:Latin (livre)]]
[[Catégorie:Verbes|latin]]
okd5gddlsxl31kq20h9mp9dm815mab5
763438
763437
2026-04-11T09:39:40Z
Chesspac
110086
/* fieri et ses composés */
763438
wikitext
text/x-wiki
==Verbes irréguliers==
=== velle et ses dérivés ===
*'''volo''', vis, velle : vouloir
*nolo, non vis, nolle : ne pas vouloir
*malo, mavis, malle : préférer
=== esse et ses dérivés ===
*'''sum''', es, esse : être
*absum, abes, abesse : être absent, être loin
*adsum, ades, adesse : être présent
*desum, dees, deesse : manquer à
*insum, ines, inesse : être dans
*intersum, interes, interesse : être entre
*obsum, obes, obesse : nuire à
*possum, poses, posse : pouvoir
*praesum, praees, praeesse : être à la tête de
*prosum, prodes, prodesse : être utile à
*subsum, subes, subesse : être sous
*supersum, superes, superesse : survivre à
=== ferre et ses dérivés ===
*'''ferre''', fero, fers, tuli, latum : porter
*adferre, adfero, attuli, allatum : apporter
*auferre, aufero, abstuli, ablatum : emporter, enlever
*conferre, confero, contuli, collatum
*offerre, offero, obtuli, oblatum : offrir
*referre, refero, rettuli, relatum : rapporter
*sufferre, suffero, sustuli, sublatum : supporter
=== ire et ses dérivés ===
*'''ire''', eo, is, ii, itum : aller
*abire, abeo, abii, abitum : s'en aller, s'éloigner
*adire, adeo, adii, aditum : aller vers
*exire, exeo, exii, exitum : sortir
*perire, pereo, perii, peritum : périr, disparaître
*redire, redeo, redii, reditum : revenir
*subire, subeo, subii, subitum : aller sous; s'avancer; envahir; subir
*transire, transeo, transii, transitum : traverser
=== fieri et ses composés ===
* '''fieri''', fio, fis : devenir
* confieri , confio, confis : avoir lieu, se produire ; être épuisé
* defieri , defit : il manque à
* infieri , infit : ça commence à, il commence à
* interfieri , interfio, interfis : être détruit
* malefieri , malefio, malefis : se faire du mal, être blessé
* superfieri, superfio, superfis : être de reste, rester
=== edere ===
*'''edo''' : conjugaison régulière existante, mais les formes irrégulières suivantes sont souvent utilisées :
** Infinitif : esse
** 2e PS présent : es, 3e PS présent : est, 2e PP présent : estis
** Radical imparfait subjonctif : essem
** Impératif : es, este
== Verbes défectifs courants ==
*aio, ait : je dis, il dit
*coepisse : commencer
*inquam, inquit : dis-je, dit-il
*meminisse : se souvenir
*noui : je sais
*odisse : haïr
{{Latin/Vocabulaire}}
[[Catégorie:Latin (livre)]]
[[Catégorie:Verbes|latin]]
3na61c6cz1heniyhz9k4uu2neg1ljx6
763439
763438
2026-04-11T09:39:54Z
Chesspac
110086
/* edere */
763439
wikitext
text/x-wiki
==Verbes irréguliers==
=== velle et ses dérivés ===
*'''volo''', vis, velle : vouloir
*nolo, non vis, nolle : ne pas vouloir
*malo, mavis, malle : préférer
=== esse et ses dérivés ===
*'''sum''', es, esse : être
*absum, abes, abesse : être absent, être loin
*adsum, ades, adesse : être présent
*desum, dees, deesse : manquer à
*insum, ines, inesse : être dans
*intersum, interes, interesse : être entre
*obsum, obes, obesse : nuire à
*possum, poses, posse : pouvoir
*praesum, praees, praeesse : être à la tête de
*prosum, prodes, prodesse : être utile à
*subsum, subes, subesse : être sous
*supersum, superes, superesse : survivre à
=== ferre et ses dérivés ===
*'''ferre''', fero, fers, tuli, latum : porter
*adferre, adfero, attuli, allatum : apporter
*auferre, aufero, abstuli, ablatum : emporter, enlever
*conferre, confero, contuli, collatum
*offerre, offero, obtuli, oblatum : offrir
*referre, refero, rettuli, relatum : rapporter
*sufferre, suffero, sustuli, sublatum : supporter
=== ire et ses dérivés ===
*'''ire''', eo, is, ii, itum : aller
*abire, abeo, abii, abitum : s'en aller, s'éloigner
*adire, adeo, adii, aditum : aller vers
*exire, exeo, exii, exitum : sortir
*perire, pereo, perii, peritum : périr, disparaître
*redire, redeo, redii, reditum : revenir
*subire, subeo, subii, subitum : aller sous; s'avancer; envahir; subir
*transire, transeo, transii, transitum : traverser
=== fieri et ses composés ===
* '''fieri''', fio, fis : devenir
* confieri , confio, confis : avoir lieu, se produire ; être épuisé
* defieri , defit : il manque à
* infieri , infit : ça commence à, il commence à
* interfieri , interfio, interfis : être détruit
* malefieri , malefio, malefis : se faire du mal, être blessé
* superfieri, superfio, superfis : être de reste, rester
=== edere ===
*'''edere''' : conjugaison régulière existante, mais les formes irrégulières suivantes sont souvent utilisées :
** Infinitif : esse
** 2e PS présent : es, 3e PS présent : est, 2e PP présent : estis
** Radical imparfait subjonctif : essem
** Impératif : es, este
== Verbes défectifs courants ==
*aio, ait : je dis, il dit
*coepisse : commencer
*inquam, inquit : dis-je, dit-il
*meminisse : se souvenir
*noui : je sais
*odisse : haïr
{{Latin/Vocabulaire}}
[[Catégorie:Latin (livre)]]
[[Catégorie:Verbes|latin]]
gsi3wc9uo728yrbfglr5p9gxma706kz
763440
763439
2026-04-11T09:40:42Z
Chesspac
110086
/* velle et ses dérivés */
763440
wikitext
text/x-wiki
==Verbes irréguliers==
=== velle et ses dérivés ===
*'''velle''', volo, vis : vouloir
* nolle, nolo, non vis : ne pas vouloir
* malle, malo, mavis : préférer
=== esse et ses dérivés ===
*'''sum''', es, esse : être
*absum, abes, abesse : être absent, être loin
*adsum, ades, adesse : être présent
*desum, dees, deesse : manquer à
*insum, ines, inesse : être dans
*intersum, interes, interesse : être entre
*obsum, obes, obesse : nuire à
*possum, poses, posse : pouvoir
*praesum, praees, praeesse : être à la tête de
*prosum, prodes, prodesse : être utile à
*subsum, subes, subesse : être sous
*supersum, superes, superesse : survivre à
=== ferre et ses dérivés ===
*'''ferre''', fero, fers, tuli, latum : porter
*adferre, adfero, attuli, allatum : apporter
*auferre, aufero, abstuli, ablatum : emporter, enlever
*conferre, confero, contuli, collatum
*offerre, offero, obtuli, oblatum : offrir
*referre, refero, rettuli, relatum : rapporter
*sufferre, suffero, sustuli, sublatum : supporter
=== ire et ses dérivés ===
*'''ire''', eo, is, ii, itum : aller
*abire, abeo, abii, abitum : s'en aller, s'éloigner
*adire, adeo, adii, aditum : aller vers
*exire, exeo, exii, exitum : sortir
*perire, pereo, perii, peritum : périr, disparaître
*redire, redeo, redii, reditum : revenir
*subire, subeo, subii, subitum : aller sous; s'avancer; envahir; subir
*transire, transeo, transii, transitum : traverser
=== fieri et ses composés ===
* '''fieri''', fio, fis : devenir
* confieri , confio, confis : avoir lieu, se produire ; être épuisé
* defieri , defit : il manque à
* infieri , infit : ça commence à, il commence à
* interfieri , interfio, interfis : être détruit
* malefieri , malefio, malefis : se faire du mal, être blessé
* superfieri, superfio, superfis : être de reste, rester
=== edere ===
*'''edere''' : conjugaison régulière existante, mais les formes irrégulières suivantes sont souvent utilisées :
** Infinitif : esse
** 2e PS présent : es, 3e PS présent : est, 2e PP présent : estis
** Radical imparfait subjonctif : essem
** Impératif : es, este
== Verbes défectifs courants ==
*aio, ait : je dis, il dit
*coepisse : commencer
*inquam, inquit : dis-je, dit-il
*meminisse : se souvenir
*noui : je sais
*odisse : haïr
{{Latin/Vocabulaire}}
[[Catégorie:Latin (livre)]]
[[Catégorie:Verbes|latin]]
e57438jverllkryi2obw2ooffh4ooaa
Liste de mnémoniques
0
12821
763414
763249
2026-04-10T17:52:50Z
~2026-22232-29
123481
/* Langues étrangères */
763414
wikitext
text/x-wiki
Cette page contient une '''liste de [[w:mnémotechnique|mnémotechniques]]''', c’est-à-dire différentes constructions qui facilitent la mémorisation.
Par exemple : afin de retenir beaucoup plus facilement les sept péchés capitaux, une phrase mnémotechnique possible offrant une image plus visuelle regroupant une partie ou la totalité d'un péché :
Par goût, Colette envie l'orgue luxueux d'Avarice
(Paresse, gourmandise, colère, envie, orgueil, luxure, avarice)
== Atmosphère ==
=== Structure verticale ===
L’atmosphère terrestre présente une structure verticale en couches basée sur l’évolution de la température. On distingue la Troposphère (siège des phénomènes météorologiques), la Stratosphère, la Mésosphère et la Thermosphère.
'''T'''out '''S'''ur '''M'''a '''T'''ête
== Mathématiques ==
=== Les 126 premières décimales du nombre π (pi) ===
Le nombre de lettres de chaque mot de ce poème correspond à une décimale de [[w:Pi|Pi]]. Un mot de dix lettres correspond au chiffre 0.
{|
|- align="center"
| ''Que'' || || ''j'' || ''<nowiki>’</nowiki>'' || ''aime'' || ''à'' || ''faire'' || ''apprendre'' || ''un'' || ''nombre'' || ''utile'' || ''aux'' || ''sages'' || ''!''
|- align="center"
! 3 !! , !! 1 !! !! 4 !! 1 !! 5 !! 9 !! 2 !! 6 !! 5 !! 3 !! 5
|}
{|
|- align="center"
| ''Immortel'' || ''Archimède'' || '','' || ''artiste'' || ''ingénieur'' || '',''
|- align="center"
! 8 !! 9 !! !! 7 !! 9
|}
{|
|- align="center"
| ''Qui'' || ''de'' || ''ton'' || ''jugement'' || ''peut'' || ''priser'' || ''la'' || ''valeur'' || ''?''
|- align="center"
! 3 !! 2 !! 3 !! 8 !! 4 !! 6 !! 2 !! 6
|}
{|
|- align="center"
| ''Pour'' || ''moi'' || '','' || ''ton'' || ''problème'' || ''eut'' || ''de'' || ''pareils'' || ''avantages'' || ''...''
|- align="center"
! 4 !! 3 !! !!3!! 8 !! 3 !! 2 !! 7 !! 9
|}
{|
|- align="center"
| ''Jadis'' || '','' || ''mystérieux'' || , || ''un'' || ''problème'' || ''bloquait''
|- align="center"
! 5 !! !! 0 !! !! 2 !! 8 !! 8
|}
{|
|- align="center"
| ''Tout'' || ''l'' || ''<nowiki>’</nowiki>'' || ''admirable'' || ''procédé'' || '','' || ''l'' || ''<nowiki>’</nowiki>'' || ''œuvre'' || ''grandiose''
|- align="center"
! 4 !! 1 !! !! 9 !! 7 !! !! 1 !! !! 6 !! 9
|}
{|
|- align="center"
| ''Que'' || ''Pythagore'' || ''découvrit'' || ''aux'' || ''anciens'' || ''Grecs'' ||''.''
|- align="center"
! 3 !! 9 !! 9 !! 3 !! 7 !! 5
|}
{|
|- align="center"
| ''Ô'' || ''quadrature'' || ''!'' || ''Vieux'' || ''tourment'' || ''du'' || ''philosophe'' || ''...''
|- align="center"
! 1 !! 0 !! !! 5 !! 8 !! 2 !! 0
|}
{|
|- align="center"
| ''Insoluble'' || ''rondeur'' || '','' || ''trop'' || ''longtemps'' || ''vous'' || ''avez''
|- align="center"
! 9 !! 7 !! !! 4 !! 9 !! 4 !! 4
|}
{|
|- align="center"
| ''Défié'' || ''Pythagore'' || ''et'' || ''ses'' || ''imitateurs'' || ''.''
|- align="center"
! 5 !! 9 !! 2 !! 3 !! 0
|}
{|
|- align="center"
| ''Comment'' || ''intégrer'' || ''l'' || ''<nowiki>’</nowiki>'' || ''espace'' || ''plan'' || ''circulaire'' || ''?''
|- align="center"
! 7 !! 8 !! 1 !! !! 6 !! 4 !! 0
|}
{|
|- align="center"
| ''Former'' || ''un'' || ''triangle'' || ''auquel'' || ''il'' || ''équivaudra'' || ''?''
|- align="center"
! 6 !! 2 !! 8 !! 6 !! 2 !! 0
|}
{|
|- align="center"
| ''Nouvelle'' || ''invention'' || '':'' || ''Archimède'' || ''inscrira''
|- align="center"
! 8 !! 9 !! !! 9 !! 8
|}
{|
|- align="center"
| ''Dedans'' || ''un'' || ''hexagone'' || '';'' || ''appréciera'' || ''son'' || ''aire''
|- align="center"
! 6 !! 2 !! 8 !! !! 0 !! 3 !! 4
|}
{|
|- align="center"
|''Fonction'' || ''du'' || ''rayon'' || ''.'' || ''Pas'' || ''trop'' || ''ne'' || ''s'' || ''<nowiki>’</nowiki>'' || ''y'' || ''tiendra''
|- align="center"
! 8 !! 2 !! 5 !! !! 3 !! 4 !!2 !! 1 !! !! 1 !! 7
|}
{|
|- align="center"
|''Dédoublera'' || ''chaque'' || ''élément'' || ''antérieur''
|- align="center"
! 0 !! 6 !! 7 !! 9
|}
{|
|- align="center"
|''Toujours'' || ''de'' || ''l'' || ''<nowiki>’</nowiki>'' || ''orbe'' || ''calculé'''e'''''¹ || ''approchera''
|- align="center"
! 8 !! 2 !! 1 !! !! 4 !! 8 !! 0
|}
{|
|- align="center"
|''Définira'' || ''limite'' || '';'' || ''enfin'' || '','' || ''l'' || ''<nowiki>’</nowiki>'' || ''arc'' || '','' || ''le'' || ''limiteur''
|- align="center"
! 8 !! 6 !! !! 5 !! !! 1 !! !! 3 !! !! 2 !! 8
|}
{|
|- align="center"
|''De'' || ''cet'' || ''inquiétant'' || ''cercle'' || '','' || ''ennemi'' || ''trop'' || ''rebelle''
|- align="center"
! 2 !! 3 !! 0 !! 6 !! !! 6 !! 4 !! 7
|}
{|
|- align="center"
|''Professeur'', || ''enseignez'' || ''son'' || ''problème'' || ''avec'' || ''zèle'' || ''!''
|- align="center"
! 0 !! 9 !! 3 !! 8 !! 4 !! 4
|}
¹ Le mot orbe est du masculin mais ce ne fut pas toujours le cas, ceci induit à présent une faute d’accord à « ''calculée'' » que l’on peut remplacer par « ''escompté'' » pour conserver le bon nombre de lettres.
=== Premières décimales de l’inverse du nombre π : 1/π ===
La valeur de '''1/π = 0,3183098''' se retient sous la forme d’une phrase historique faisant référence aux trois glorieuses :
{{Citation|« Les 3 journées de 1830 ont renversé 89 [la Révolution de 1789] »}}.
Le score de la finale de la coupe du monde de football 98 a fait '''un surpris''' (1 sur pi), côté Brésil : '''0''', '''3''' (c’est le feu ! = '''18''' = '''''Cher''''' payé ?) ; '''3-0''' ('''''Gard'''''ez en souvenir) '''98'''
=== Trigonométrie ===
Le principe est de ne retenir que la première lettre ou la première syllabe des mots-clés de chaque définition ou théorème :
==== Définitions ====
* « Cosinus = côté Adjacent sur l'Hypoténuse »
* « Sinus = côté Opposé sur l'Hypoténuse »
* « Tangente = côté Opposé sur côté Adjacent »
Une "phrase" permet de se rappeler ces trois définitions à la fois :
'''cah soh toa''' pour « ''casse-toi'' » : '''C'''osinus = '''A'''djacent sur '''H'''ypoténuse ; '''S'''inus = '''O'''pposé sur '''H'''ypoténuse ; '''T'''angente = '''O'''pposé sur '''A'''djacent. Certains préfèrent '''soh cah toa'''.
On peut aussi ressortir les dénominateurs de chaque fraction (afin de ne pas mélanger numérateurs et dénominateurs dans ces égalités) en apprenant les sons : '''SO-CA-TO, H-H-A''' (HHA étant les dénominateurs : '''S'''in ='''O'''pp /''H''yp , '''C'''os = '''A'''dj/''H''yp, '''T'''an='''O'''pp/''A''dj)
<br />
D'autres méthodes consistent à associer un "mot" facile à retenir à chacune des trois définitions:<br />
- Cosadi - Sinopi - Tanopad <br />
- cosadjip - sinopip - tangopaj <br />
- CAHier - SOHo - TOAst (ou COCA)
==== Théorèmes ====
* « sin (a+b) = sin a cos b + cos a sin b » devient « ''sico cosi'' »
* « cos (a+b) = cos a cos b - sin a sin b » devient « ''coco sisi'' » (ou « ''coco MOINS sisi'' ou « ''coco ISsi'' » pour retenir le signe)
* À noter que la formule « ''sico cosi / coco moins sisi'' » ou « ''Coco si méchant, si, Coco, si'' » permet également d’apprendre les formules de factorisation suivantes :
sin p + sin q = 2 sin [(p+q)/2] •cos [(p-q)/2]
sin p - sin q = 2 cos [(p+q)/2] •sin [(p-q)/2]
cos p + cos q = 2 cos [(p+q)/2] •cos [(p-q)/2]
cos p - cos q = -2 sin [(p+q)/2] •sin [(p-q)/2]
Avec p = A + B et q = A - B
'''Cosinus est menteur et raciste''' ('''CO'''s comme '''CO'''n) en effet cos (a+b) donne (cos a cos b) - (sin a sin b). Cosinus est donc menteur puisque le signe de l’addition (positive) est négatif. Cosinus est raciste puisque on obtient (cos a cos b) d’une part et (sin a sin b) d’autre part : les cosinus et les sinus ne se mélangent pas.
'''co'''sinus est un '''co'''pain '''co'''n (copain pour le sens et con pour le signe): cos(a'''+'''b)= cos(a)cos(b) '''-''' sin(a)sin(b) : les cosinus restent ensemble, mais le signe change.
'''s'''inus est une '''s'''alade '''s'''ympa (salade pour le sens et sympa pour le signe): sin (a'''+'''b)=sin(a)cos(b) '''+''' sin(b)cos(a) : sin et cos se mélangent mais le signe reste le même
On trouve également : '''opip adjip opadj''' : sinus ('''op'''posé sur '''hyp'''oténuse), cosinus ('''adj'''acent sur '''hyp'''oténuse), tangente ('''op'''posé sur '''adj'''acent). La phrase prononcée rapidement d’un seul coup est très facile à mémoriser.
De même que '''SOH CAH TOA''': '''S'''inus= '''O'''pposé sur '''H'''ypoténuse '''C'''osinus= '''A'''djacent sur '''H'''ypoténuse '''T'''angente= '''O'''pposé sur '''A'''djacent
On peut lui substituer la formule plus percutante : '''CAH SOH TOA''' (à prononcer Casse toi ! )
=== Dates et constantes ===
Le [[w:code chiffres-sons|code chiffres-sons]] est une méthode qui permet de se souvenir de dates ou de valeurs numériques en formant des phrases.
=== Formules de géométrie ===
* Circonférence d’un cercle : 2 pi R (2 pierres)
** La circonférence est toute fière d’être égale à 2 pi R
* Aire d’un disque: pi R<sup>2</sup> (« pierre carrée » ou « pierre deux »)
** Le cercle est tout joyeux d’être égal à pi R<sup>2</sup> (prononcer « pi R deux »)
* « Le volume de la sphère, est quoi qu’on y puisse faire, 4/3 pi R<sup>3</sup>, fut-elle de bois. » ([[w:fr:Marcel Pagnol|Marcel Pagnol]])
Le volume d'une sphère, qu'elle soit de pierre, qu'elle soit de bois est égal aux 4/3 de pi R3
* Le volume d'une pizza (d'un camembert, ou de n'importe quel objet semblable) de rayon 'z' et de hauteur 'a' est égale à '''Pi.(z.z).a''' (la formule correspond à son nom)
soit V = π.z<sup>2</sup>.a
=== Analyse vectorielle ===
[[File:DRG chart fr.svg|thumb|right|300px|Diagramme des principales relations entre opérateurs de calcul vectoriel.]]
* Opérateurs s'annulant: '''DiR'''i'''G'''é (décrivant les flèches centrales sur le diagramme à droite)
** '''DiR'''i: <math>\mathrm{div}(\overrightarrow{\mathrm{rot}})=0</math>
** '''R'''i'''G''': <math>\overrightarrow{\mathrm{rot}}(\overrightarrow{\mathrm{grad}})=\vec{0}</math>
* Autres formules (flèches reliant div et grad sur le diagramme à droite):
** <math>\Delta = \mathrm{div}(\overrightarrow{\mathrm{grad}})</math>
** <math>\overrightarrow{\mathrm{grad}}(\mathrm{div})= \overrightarrow{\mathrm{rot}}(\overrightarrow{\mathrm{rot}})+\vec{\Delta}</math>
=== Ordre des opérations ===
En algèbre, les opérations simples : <code>(</code> <code>)</code>, <code>+</code>, <code>-</code>, <code>×</code> et <code>÷</code>, sont évaluées selon un certain ordre : '''PEMDAS''' pour « '''p'''arenthèses, '''e'''xposant,
'''m'''ultiplication, '''d'''ivision, '''a'''ddition et '''s'''oustraction ». Pour plus de détails sur l'application de ce mnémonique, voir [[:w:fr:Ordre des opérations|Ordre des opérations]].
=== Double distributivité ===
Retenir le mot « '''PIED''' » qui donne les termes à regrouper lorsque l’on développe : '''P'''remiers, '''I'''ntérieurs, '''E'''xtérieurs, '''D'''erniers.
=== Constante e<ref name="villemin.gerard">http://villemin.gerard.free.fr/Wwwgvmm/MnemoTe/Phrase.htm</ref> ===
{| border="0" cellpadding="0" cellspacing="1"
|align="center"|Tu
|
|align="center"|aideras
|
|align="center"|à
|
|align="center"|rappeler
|
|align="center"|ta
|
|align="center"|quantité
|
|align="center"|à
|
|align="center"|beaucoup
|
|align="center"|de
|
|align="center"|docteurs
|
|align="center"|amis.
|-
|align="center"|2
| ,
|align="center"|7
|
|align="center"|1
|
|align="center"|8
|
| align="center" |2
|
|align="center"|8
|
|align="center"|1
|
|align="center"|8
|
|align="center"|2
|
|align="center"|8
|
|align="center"|4
|}
=== Nombre d'or<ref name="villemin.gerard"/> ===
{| border="0" cellpadding="0" cellspacing="1"
|align="center"|Ô
|
|align="center"|nombre
|
|align="center"|d'
|
|align="center"|élégance
|
|align="center"|!
|
|align="center"|Toi,
|
|align="center"|toi,
|
|align="center"|grandiose,
|
|align="center"|étonnant :
|
|align="center"|''le nombre d'or''.
|-
|align="center"|1
| ,
|align="center"|6
|
|align="center"|1
|
|align="center"|8
|
|align="center"|0
|
|align="center"|3
|
|align="center"|3
|
|align="center"|9
|
|align="center"|8
|
|align="center"|
|}
(! pour 0)
=== Statistiques ===
* [[w:Règle 68-95-99.7|Règle 68-95-99.7]] : la proportion des échantillons entre [-σ, +σ], [-2σ, +2σ], [-3σ, +3σ] pour une distribution gaussienne centrée.
* Erreurs de première espèce et deuxième espèce.
** Se rappeler la fable d’Ésope dans laquelle un enfant [[wikt:crier au loup|crie au loup]] (hypothèse nulle <math>H_0</math>: « il n'y a aucun loup »).
**# D'abord, les villageois pensent qu'il y a un loup alors qu'il n'y en a aucun (erreur de première espèce).
**# Puis, les villageois pensent qu'il n'y a aucun loup alors qu'il y en a un (erreur de seconde espèce).
** Il y a une barre dans '''P'''ositif (faux positif : erreur de type '''I''') et deux barres dans '''N'''égatif (faux négatif : erreur de type '''II''').
== Sciences ==
=== Astronomie ===
==== Ordre des planètes du Système solaire ====
Il existe toute une série de termes mnémotechniques pour se souvenir de l'ordre des planètes à l’intérieur du [[w:système solaire|Système solaire]]. La première lettre de chaque mot de cette phrase correspond à la première lettre de chaque [[w:planète|planète]], de la plus rapprochée à la plus éloignée du Soleil. L'[[w:apostrophe|apostrophe]] ou la [[w:virgule|virgule]] peut représenter la [[w:ceinture d'astéroïdes|ceinture d'astéroïdes]] entre [[w:Mars (planète)|Mars]] et [[w:Jupiter|Jupiter]].
Voici l’ordre des planètes du Système solaire :
Mercure, Vénus, Terre, Mars, Jupiter, Saturne, Uranus, Neptune.
''À NOTER que selon la [[w:Définition des planètes de l'UAI|nouvelle définition]] de l’[[w:Union astronomique internationale|Union astronomique internationale]] d’août [[w:2006|2006]], [[w:Pluton (planète naine)|Pluton]] n’est plus considérée comme une [[w:planète|planète]] mais comme une planète naine ([[w:(134340) Pluton|(134340) Pluton]])'' (de même que [[w:(1) Cérès|(1) Cérès]], [[w:(136199) Éris|(136199) Éris]], [[w:(136108) Haumea|(136108) Haumea]] et [[w:(136472) Makemake|(136472) Makemake]]), <br/>
'''''Ordre des planètes du Système solaire : '''Mercure, Vénus, Terre, Mars, Jupiter, Saturne, Uranus, Neptune.''
On emploie par exemple les phrases suivantes :
*'''''M'''ême '''V'''ieux '''T'''ruc '''M'''ais '''J''''en '''S'''ais '''U'''n '''N'''ouveau.''
*'''''M'''a '''V'''ieille '''T'''ante '''M'''arie a '''J'''eté '''S'''amedi '''U'''n '''N'''avet.''
*'''''M'''a '''V'''ieille '''T'''rompette '''M'''e '''J'''oue '''S'''on '''U'''ltime '''N'''octurne.''
*'''''M'''a '''V'''oiture '''T'''e '''M'''ène '''J'''oyeusement '''S'''ur '''U'''ne '''N'''ationale.''
*'''''M'''arie, '''V'''iendras-'''T'''u '''M'''anger '''J'''eudi '''S'''ur '''U'''ne '''N'''appe ?''
*'''''M'''angez '''V'''os '''T'''artes, '''M'''ais '''J'''uste '''S'''ur '''U'''ne '''N'''appe .''
*'''''M'''e '''V'''oici '''T'''oute '''M'''ignonne''',''' '''J'''e '''S'''uis '''U'''ne '''N'''ébuleuse.''
*'''''M'''e '''V'''oici '''T'''oute '''M'''odifiée''',''' '''J'''e '''S'''uis '''U'''ne '''N'''ouveauté.''
*'''''M'''e '''V'''oilà '''T'''out '''M'''ouillé''',''' '''J''''ai '''S'''uivi '''U'''n '''N'''uage.''
*'''''M'''e '''V'''oilà '''T'''oute '''M'''ouillée''',''' '''J'''e '''S'''uis '''U'''ne '''N'''ymphomane.''
*'''''M'''on '''V'''ieux, '''T'''u '''M''''as '''J'''eté '''S'''ur '''U'''ne '''N'''avette.''
*'''''M'''on '''V'''ioloncelle '''T'''ombe, '''M'''ais '''J'''e '''S'''auve '''U'''ne '''N'''ote.
*'''''M'''aman '''V'''ole '''T'''ous '''M'''es '''J'''ouets, '''S'''auf '''U'''n '''N'''ounours !''
*'''''M'''e '''V'''oici, '''T'''onton '''M'''arcel, '''J'''e '''S'''uis '''U'''n '''N'''ageur.''
*'''''M'''a '''V'''ille '''T'''hionville '''M'''ontre '''J'''oyeusement '''S'''on '''U'''nivers '''N'''octurne.''
*'''''M'''on '''V'''élo '''T'''e '''M'''ènera ''' J'''usque '''S'''ur '''U'''n '''N'''uage.''
*'''''M'''on '''V'''élo '''T'''ourne '''M'''al, ''' J''''en '''S'''ouhaite '''U'''n '''N'''ouveau.''
*'''''M'''onsieur, '''V'''ous '''T'''ravaillez '''M'''al ; - ''' J'''e '''S'''uis '''U'''n '''N'''ovice.''
*'''''M'''al '''V'''êtu '''T'''oi '''M'''ême, '''J'''e '''S'''uis '''U'''n '''N'''udiste''
*'''''M'''on '''V'''ieux '''T'''outou '''M'''édor '''J'''oue '''S'''ur '''U'''n '''N'''uage''
*'''''M'''a '''V'''erge '''T'''e '''M'''ènera '''J'''usque '''S'''ur '''U'''n '''N'''uage''
*'''''M'''ais '''V'''ous '''T'''ombez '''M'''al, '''J''''ai '''S'''auté '''U'''ne '''N'''aine''
* '''''M'''essieurs!''' V'''otre '''T'''rahison '''M'''<nowiki/><nowiki>'écœure: </nowiki>'''J'''ouer''' S'''ur '''U'''ne '''N'''omenclature!'' (au sujet de la disparition de Pluton de la liste)
*'''''S'''ors (pour Soleil) -'''M'''oi '''V'''ite '''T'''a '''M'''armite '''J'''aune '''S'''ur '''U'''ne '''N'''appe.''
*'''''M'''arquez '''V'''otre '''T'''emps '''M'''esuré '''J'''uste '''S'''ous '''U'''ne '''N'''anoseconde.''
'''Et pour les nostalgiques de Pluton...'''
* '''''M'''anon '''V'''iendras '''T'''u '''M'''anger '''J'''eudi '''S'''ur '''U'''ne '''N'''appe '''P'''ropre.''
*'''''M'''ercure '''V'''eut '''T'''aquiner '''M'''ars, '''J'''e '''S'''uis '''U'''ne '''N'''ouvelle '''P'''lanète.''
*'''''M'''ademoiselle, '''V'''ous '''T'''ravaillez '''M'''al, '''J'''e '''S'''uis '''U'''n '''N'''ouveau '''P'''rofesseur''
*''Le '''M'''onde '''V'''oit '''T'''ourner du '''M'''atin '''J'''usqu'au '''S'''oir '''U'''niquement '''N'''euf '''P'''lanètes''.
*'''''M'''on '''V'''ieux, '''T'''u '''M'''e '''J'''ettes '''S'''ur '''U'''ne '''N'''ouvelle '''P'''lanète.''
*'''''M'''onsieur '''V'''euillez '''T'''ournez '''M'''a '''J'''upe '''S'''ans '''U'''ne '''N'''aïve '''P'''udeur.''
*'''''M'''e '''V'''oici, '''T'''oute '''M'''ignonne, '''J'''e '''S'''uis '''U'''ne '''N'''ouvelle '''P'''lanète.
*'''''M'''e '''V'''oici, '''T'''onton '''M'''arcel, '''J'''e '''S'''uis '''U'''n '''N'''ageur '''P'''rofessionnel.
*'''''M'''e '''V'''oici, '''T'''out '''M'''ouillé, '''J'''e '''S'''uis '''U'''n '''N'''ageur '''P'''ressé.
*'''''M'''e '''V'''oici, '''T'''out '''M'''ouillé, '''J''''ai '''S'''uivi '''U'''n '''N'''uage '''P'''luvieux.
*'''''M'''ais '''V'''iendras-'''T'''u '''M'''anger, '''J'''ulie, '''S'''ur '''U'''ne '''N'''appe '''P'''ropre.
*'''''M'''on '''V'''ieux '''T'''héâtre '''M'''e '''J'''oue '''S'''ouvent '''U'''ne '''N'''ouvelle '''P'''ièce.
*'''''M'''on '''V'''ieux, '''T'''u '''M''''as '''J'''eté '''S'''ur '''U'''ne '''N'''ouvelle '''P'''lanète.
*'''''S'''ors (pour Soleil) -'''M'''oi '''V'''ite '''T'''a '''M'''armite '''J'''aune '''S'''ur '''U'''ne '''N'''appe '''P'''ropre.''
*'''''M'''onsieur '''V'''ous '''T'''irez '''M'''al '''J'''e '''S'''uis '''U'''n '''N'''ovice '''P'''itoyable.
* '''''Mé'''lanie, '''V'''ous '''T'''ombez '''Ma'''l, '''J'''e '''S'''uis '''U'''n '''N'''avet '''P'''ourri.''
*'''''M'''on '''V'''aisseau '''T'''e '''M'''ènera '''J'''eudi '''S'''ur '''U'''ne '''N'''ouvelle '''P'''lanète.
*'''''M'''a '''V'''ieille '''T'''ante '''M'''arge '''J'''oue '''S'''ur '''U'''n '''N'''ouveau '''P'''iano.
*'''''M'''aman '''V'''ole '''T'''ous '''M'''es '''J'''ouets, '''S'''auf '''U'''n '''N'''ounours '''P'''ourri !''
*'''''M'''arin '''V'''aleureux, '''T'''u '''M'''ourras '''J'''eune '''S'''ur '''U'''n '''N'''avire '''P'''erdu !''
*'''''M'''on '''V'''élo '''T'''ourne '''M'''al '''J'''e '''S'''uis '''U'''n '''N'''ouveau '''P'''iéton.
*'''''M'''erci '''V'''ous '''T'''ous '''M'''aintenant '''J'''e '''S'''ais '''U'''nir '''N'''euf '''P'''lanètes.
*'''''M'''élanie '''V'''ous '''T'''ombez '''M'''al '''J'''e '''S'''uis '''U'''n '''N'''avet '''P'''ourri.
*'''''M'''es '''V'''ieilles '''T'''antes '''M'''angeaient '''J'''adis '''S'''ur '''U'''ne '''N'''appe '''P'''ercée.''
*'''''M'''ets '''V'''ite '''T'''on '''M'''aillot '''J'''e '''S'''uis '''U'''n '''N'''udiste '''P'''oilu.
* '''''M'''on '''V'''ieux '''T'''acot '''M'''´a '''J'''eté '''S'''ur '''U'''n '''N'''oble '''P'''''assant.
*'''''M'''ange '''V'''ite '''T'''on '''M'''ars '''J''' 'en '''S'''ors '''U'''n '''N'''ouveau '''P'''aquet.
*'''''MÈR'''E, '''V'''iens '''Ter'''miner '''M'''a '''JUP'''e, '''SA''' cout'''UR'''e '''NE''' tient '''PLU'''s.''
* '''''M'''aman, '''V'''oudrais-'''T'''u '''M''''emmener '''J'''ouer '''S'''ur '''U'''ne '''N'''ouvelle '''P'''lanète ?''
Suite à un concours qui s’est déroulé au Québec, la formule suivante a été retenue :
*'''''M'''angez '''V'''os '''T'''artes, '''M'''ais '''J'''uste '''S'''ur '''U'''ne '''N'''appe !''
Il existe aussi cette formule (la plus ancienne mnémonique connue en astronomie) qui se retient facilement, grâce à ses trois mots de trois syllabes :
*''Merveter, Marjusa, Uneplu''
('''Mer'''cure, '''Vé'''nus, '''Ter'''re, '''Mar'''s, '''Ju'''piter, '''Sa'''turne, '''U'''ranus, '''Nep'''tune, '''Plu'''ton)
Une variante<ref>Formule tirée de l’un des tomes du [https://fr.m.wikipedia.org/wiki/Manuel_des_Castors_Juniors ''Manuel des Castors Juniors'']</ref> de celle-ci :
* ''Mervé'', ''Termaju'', ''Saturneplu''
('''Mer'''cure, '''Vé'''nus, '''Ter'''re, '''Ma'''rs, '''Ju'''piter, '''Sat'''urne, '''Ur'''anus, '''Ne'''ptune, '''Plu'''ton)
Qui existe aussi sous cette forme :
* ''Mervé, Termaju, Satur n'est plus''
Et celle-ci qui inclut le Soleil :
*'''''S'''alut ! '''Me''' '''v'''ois-'''t'''u ? '''M'''oi '''j'''e '''s'''uis '''u'''ne '''n'''ouvelle '''pl'''anète !''
'''Planète ayant un système d'anneaux'''
* '''J'''e '''S'''uis '''U'''ne '''N'''ouille (Jupiter, Saturne, Uranus, Neptune)
==== Ordre des quatre lunes principales de Jupiter ====
'''I'''l '''e'''st '''g'''rand, '''C'''harles !
* [[w:Io|Io]], [[w:Europe|Europe]], [[w:Ganymède_(lune)|Ganymède]], [[w:Callisto_(lune)|Callisto]]
==== Croissant de Lune ====
Le '''p'''remier croissant et le '''d'''ernier croissant peuvent être reconnus en les assimilant aux sens du p et du d. En effet, en « ajoutant » au croissant de lune un bâton, on obtient un p ou un d selon le croissant. Cette méthode marche uniquement dans l'[[w:Hémisphère (géographie)|hémisphère]] [[w:nord|nord]], dans l’hémisphère sud il faudra considérer que la Lune ment.
Une méthode plus simpliste consistait autrefois à lire le croissant de lune directement. Quand il formait un '''C''' la lune incitait à penser qu'elle était '''C'''roissante . Or dans ce cas là elle est décroissante. Et quand elle formait un '''D''' (en supposant l’ajout de la barre droite nécessaire) elle incitait à penser qu’elle était '''D'''écroissante. Or dans ce cas là elle est croissante. Il en est venu l’expression populaire : ''Il est menteur comme la lune''. Cependant, dans ce cas la Lune ne ment que dans l'hémisphère Nord : C correspond bien à la Lune croissante et D à la Lune décroissante.
Ces méthodes ne sont plus valables autour de l’[[w:Équateur (ligne équinoxiale)|équateur]], ou le sens de ''lecture'' varie selon les saisons.
==== [[w:type spectral|Types spectraux]] [[w:étoile|stellaires]] ====
Les différents [[w:type spectral|types spectraux]], du plus chaud au plus froid, sont : O, B, A, F, G, K, M.
'''''O'''h, '''b'''e '''a''' '''f'''ine '''g'''irl/'''g'''uy, '''k'''iss '''m'''e !''
'''''O'''verseas '''b'''roadcast: '''a''' '''f'''lash! [[w:Godzilla|'''G'''odzilla]] '''k'''ills [[w:Mothra|'''M'''othra]] !''
=== Physique ===
==== Électromagnétique ====
Énergie électrique stockée dans un condensateur :
<math>E= (1/2) C U^2</math>
"l'''e''' '''demi''' '''cu'''l '''carré'''"
==== Les sept unités fondamentales====
Pour: ''seconde, ampère, candela, kilogramme, mètre, kelvin, mole '':
<br> Sac km km
<br> Je <u>'''s'''ais q</u>u<u>'''a'''n</u>d <u>'''c'''a</u>c<u>'''k'''i</u> <u>'''m'''et</u> <u>'''k'''el</u> <u>'''m'''o</u>t ! : (je) ''Sec-Am-Ca-Ki-Mè-Kel-Mo''
<br> <u>Ce con</u> d'<U>Ampère</U>, <u>qu'en d</u>it <u>qui l'au</u>ra!, <u>mettr</u>a <u>quel vin</u> au <u>môl</u>e ?: ''Sec<s>onde</s>'' d' ''ampère'', ''cand<s>ela</s>'' ''kilo<s>gramme</s>''ra, ''mètr<s>e</s>''a ''kelvin'' au ''moles''
"Secondes molles, quand des lacs-îlots [[wikt:grammer|grammant]] [[wikt:pairer|pairent]], Maître Kelvin !" (qui se prononce comme : "seconde, mole, candela, kilogramme, ampère, mètre, kelvin")
*
==== Ordre des couleurs de [[w:Résistance (composant)#Repérage et valeurs normalisées|résistance électrique]] ====
('''N'''oir, '''M'''arron, '''R'''ouge, '''O'''range, '''J'''aune, '''V'''ert, '''B'''leu, '''Vio'''let, '''G'''ris, '''B'''lanc)
'''''N'''e '''M'''ange '''R'''ien '''O'''u '''J'''e '''V'''ais '''B'''leuir '''V'''iolemment (ton) '''G'''ros '''B'''laze.''
'''''N'''e '''M'''angez '''R'''ien '''O'''u '''J'''e '''V'''ous '''B'''rule '''V'''otre '''G'''rosse '''B'''arbe.''
'''''N'''e '''M'''angez '''R'''ien '''O'''u '''J'''eûnez, '''V'''oilà '''B'''ien '''V'''otre '''G'''rande '''B'''êtise.''
'''''N'''e '''M'''angez '''R'''ien '''O'''u '''J'''e '''V'''ous '''B'''rise '''V'''otre '''G'''rosse '''B'''outeille.''
'''N'''e '''M'''angez '''R'''ien '''O'''u '''J'''e '''V'''ous '''B'''ats '''V'''iolemment '''G'''ros '''B'''êta
'''''N'''adine '''M'''e '''R'''épondit''' O'''ui, '''J'''e '''V'''eux '''B'''ien '''V'''otre '''G'''rosse '''B'''iroute''
'''''N'''oir, '''M'''arron, les couleurs de l'arc en ciel (sauf l'indigo), '''G'''ris, '''B'''lanc.''
Version québécoise utilisant la lettre '''B''' pour Brun au lieu de Marron :
'''''N'''otre '''B'''ar '''R'''estera '''O'''uvert '''J'''eudi et '''V'''endredi. '''B'''ière et '''V'''in '''G'''ratuit, '''B'''ienvenue.''
'''''N'''otre '''B'''ar '''R'''estera '''O'''uvert '''J'''eudi et '''V'''endredi. '''B'''ien'''V'''enue '''G'''ros '''B'''uveur.''
==== Ordre des couleurs du [[w:Couleur#Le spectre lumineux|spectre visible]] ====
Les sept couleurs du spectre visible ou de l'arc-en-ciel
(dans l'ordre des fréquences croissantes : '''R'''ouge - '''O'''range - '''J'''aune - '''V'''ert - '''B'''leu - '''I'''ndigo - '''V'''iolet)
peuvent se retenir grâce à la phrase suivante :
La '''ROU'''sse '''OR'''ienta le '''J'''uge '''VER'''s le '''BL'''azer de l''''INDI'''enne '''VIOL'''ée.
Dans l'ordre inverse (soit de la plus petite à la la plus grande longueur d'onde) elles peuvent se retenir grâce au mot '''VIBUJOR''', en remplaçant le '''U''' par un '''V''' ('''vert''' comme '''Hu'''lk).
'''V'''iolet - '''I'''ndigo - '''B'''leu - '''V'''ert - '''J'''aune - '''O'''range - '''R'''ouge
Remarque : en se figurant le drapeau français '''bleu'''-'''blanc'''-'''rouge''', on peut retrouver l’ordre des longueurs d’onde, en assimilant le bleu à l’ultraviolet, le blanc au visible, et le rouge à l’infra-rouge : '''ultraviolet'''-'''visible'''-'''infra-rouge'''.
==== Longueur d'onde des couleurs ====
Le mot rouge est plus long que le mot bleu (5 vs 4), sa longueur d'onde est plus longue également.
==== Couleurs en peinture et rayonnements lumineux ====
Les peintres utilisent les trois couleurs fondamentales '''Cyan Magenta et Jaune''', chacune absorbant une seule des trois couleurs fondamentales de la lumière (Rouge Vert et Bleu). Notre œil ne reconnaît la couleur que par la lumière identifiée par chacune des 3 familles de cônes de l'œil respectivement sensibles aux rayonnements '''Rouge Vert et Bleu'''.
Cette phrase permet aux peintres et aux physiciens d'identifier un équivalent des deux couleurs de rayonnement lumineux identifiées par les cônes de l'œil pour chacune des couleurs fondamentales de la peinture.
'''C'''ette '''B'''onne '''V'''ieille<br />
'''M'''ijote des '''R'''aviolis "'''B'''uitoni"<br />
'''<nowiki>J'</nowiki>'''en '''R'''é'''V'''ais
La couleur '''C'''yan de la peinture correspond ainsi à la réception des rayonnements lumineux '''B'''leu et '''V'''ert.
Le '''M'''agenta, pour sa part correspond aux rayonnements lumineux '''R'''ouge et '''B'''leu.
Quant à la couleur '''J'''aune, elle renvoie vers l'œil les rayonnements lumineux '''R'''ouge et '''V'''ert.
==== Constantes ====
* vitesse de la lumière<ref name="villemin.gerard" /> :
:{| style="text-align: center;"
|Ah,||messagère||admirable,||lumière||éclatante,||je||sais||votre||célérité||
|-
|La||constante||lumineuse||restera||désormais||là||dans||votre||cervelle||
|-
|2||9||9||7||9||2||4||5||8||m/s
|}
* définition formelle d'une seconde (périodes de la radiation correspondant à une transition entre les deux sous-niveaux hyperfins du césium 133) :
:{| style="text-align: center;"
|« Pharaonne,||j'||affirmais||là,||honore||mal||l'||aimable||seconde||0 ! »
|-
|9||1||9||2||6||3||1||7||7||0
|}
=== Chimie ===
'''Priorité des groupes caractéristiques en nomenclature'''
Pour nommer une molécule composée de plusieurs groupes caractéristiques, on utilise l'ordre suivant :
Acide carboxylique - anhydride d'acide - ester - halogénure d'acyle - amide - nitrile - aldéhyde - cétone - alcool - amine - alcyne - alcène - éther-oxyde - dérivé halogéné - alcane
Pour se rappeler de l'ordre :
'''Ac'''e '''an'''nule '''Ester''' ! '''Halo ami'''? '''Ni'''e '''al'''ors '''Cé'''cile '''alcool'''isée, '''Am'''élie '''ascene''' à N'''eoxi''' et '''De'''lp'''h'''ine un '''alcane'''
==== Radicaux alkyles ====
Pour se rappeler l’ordre des 3 premiers groupement alcanes :
* Il ai'''mait''' '''êt'''re '''pro'''pre. (Oralement, "Il ai'''Mét Éth Prop''' ")
Pour se rappeler l’ordre des 4 premiers groupement alcanes :
* ('''M'''éthane, '''É'''thane, '''P'''ropane, '''B'''utane)
* '''M'''aman '''Et''' '''P'''apa '''B'''ébé
* '''M'''aman '''Et''' '''P'''apa '''But'''inent.
* '''M'''alin qui '''É'''tudie '''P'''our le '''B'''ac.
* '''M'''ieux '''É'''tudier '''P'''our le '''B'''ac.
* '''M'''on '''É'''cole '''P'''eut '''B'''rûler.
* '''M'''on '''É'''lève '''P'''isse '''B'''ien
* '''M'''organe '''E'''st '''P'''as '''B'''elle.
* '''Me'''s '''é'''lèves '''p'''arlent '''b'''eaucoup.
* '''M'''ets '''t'''es '''Prop'''res '''But'''s ! (Oralement, "Mét Éth Prop But")
Pour se rappeler l’ordre des 5 premiers groupement alcanes :
* ('''M'''éthane, '''É'''thane, '''P'''ropane, '''B'''utane, '''P'''entane)
* '''M'''aman '''E'''st '''P'''artie '''B'''ébé '''P'''leure
* '''M'''amie '''E'''st '''P'''artie '''B'''oire une '''P'''inte
Pour se rappeler l’ordre des 6 premiers groupement alcanes :
* ('''M'''éthane, '''É'''thane, '''P'''ropane, '''B'''utane, '''P'''entane, '''H'''exane)
* '''M'''aurice '''E'''st '''P'''as '''B'''eau '''P'''our '''H'''élène.
* '''M'''amie '''E'''t '''P'''api '''B'''atifolent '''P'''endant l''''H'''iver.
* '''M'''aman '''E'''t '''P'''apa '''B'''oivent '''P'''endant '''H'''alloween
* '''M'''amie '''E'''st '''P'''artie '''B'''oire une '''P'''inte de '''H'''eineken
* '''M'''et '''E'''th '''P'''rop '''B'''ut '''P'''ent '''H'''ex
Pour les plus vulgaires :
* '''M'''audite '''É'''paisse ! '''P'''ourquoi '''B'''aiser '''P'''our l' '''H'''iver !
==== [[w:Tableau périodique des éléments|Tableau périodique des éléments]] ====
Il est à noter que la plupart des moyens mnémotechniques concernant les éléments ont été créés par des [[w:étudiant|étudiant]]s, d’où le [[w:vocabulaire|vocabulaire]] parfois amusant des maximes.
===== [[w:Éléments de la période 2|Période 2]] =====
'''Pour : Li'''thium, '''Bé'''ryllium, '''B'''ore, '''C'''arbone, '''N'''itrogène (Azote), '''O'''xygène, '''F'''luor, '''Né'''on.''
* «La '''Li''''''Bé'''llule '''B'''leue, d’une '''C'''aresse, '''N'''oit dans l’'''O'''nde la '''F'''leur de '''Né'''nuphare. »
* « '''Li'''thus et '''Be'''rénice '''B'''oivent, '''C'''haque '''N'''uit, '''O''' '''F'''rais de '''Né'''ron »
* « '''Li'''verpool, '''Be'''rceau des '''B'''eatles, '''C'''onnait '''N'''aturellement ces '''O'''librius '''F'''ous et '''Né'''vrosés »
* « '''Li'''bérez '''Be'''n '''B'''arkans, '''C'''élèbre '''N'''arrateur, '''O'''u '''F'''usillez '''Né'''ron »
* « '''Li''' '''Be''' le '''B'''on '''C'''anard du '''N'''ord '''O'''uest de la '''F'''rance '''Ne'''ogauchiste »
* « '''Li'''li '''Be'''sa '''B'''ien '''C'''ouchée '''N'''ue '''O''' '''F'''lanc de '''Né'''ron »
* « '''Li'''li '''Be'''se '''B'''ien '''C'''hez '''N'''otre '''O'''ncle '''F'''umeur de '''Ne'''squik »
* « '''Li'''li '''Be'''se '''B'''ien '''C'''onfortablement '''N'''otre '''O'''ncle '''F'''rançois '''Ne'''stor »
* « '''Li'''mace '''Be'''te '''B'''ouffa '''C'''inq '''N'''ouveaux '''O'''ignons '''F'''raîchement '''Né'''s »
* « '''Li'''li '''Be'''rça '''B'''ébé '''C'''hez '''N'''otre '''O'''ncle '''F'''ernand '''N'''estor
* « '''Li'''li '''B'''ecta '''B'''ien '''C'''hez '''N'''otre '''O'''ncle '''F'''erdinand '''N'''estor »
* « '''Li'''li '''Bé'''cha '''B'''ien '''C'''hez '''N'''otre '''O'''ncle '''F'''rançois-(ou '''F'''erdinand-)'''Ne'''stor »<br />
* « '''Li'''li '''Bé'''se '''B'''ien '''C'''hez '''N'''otre '''O'''ncle '''F'''rançois-(ou '''F'''erdinand-)'''Ne'''stor »<br />
* « '''LiBe'''rté '''B'''afouée '''C'''ontre '''N'''otre '''O'''rganisation '''F'''édérale '''Né'''ogaulliste (ou '''Né'''otrotskiste) »
* « le '''Li''' t de '''BE''' '''B''' é a '''C'''assé le '''N'''ez de l' '''O'''ncle '''F'''urieux '''Né'''on »
* « '''Li'''vrez '''Bê'''tement '''B'''ataille '''C'''ar '''N'''ous, '''O'''fficiers '''F'''rançais, '''Né'''gocions »
* « '''L'i'''magination '''Be'''lliqueuse '''B'''aissa '''C'''ar '''N'''otre '''O'''rdre '''F'''ut '''Ne'''t »
* « '''Li'''re '''Be'''aucoup '''B'''alzac '''C'''ar '''N'''otre '''O'''rthographe '''F'''ait '''Né'''gligé »
* « '''Li'''ste de '''Be'''lles '''B'''outeilles de '''C'''ognac '''N'''ous '''O'''nt '''F'''outus '''Ne'''rveux »
* « '''Li'''li '''Be'''cote '''B'''ien '''C'''omme '''Ni'''cole '''O''' '''F'''ond '''N'e'''st ce pas »
* « '''Li'''bérez '''Be'''rnard '''B'''ossu '''C'''ontre '''N'''ouvel '''O'''tage '''F'''éminin. Signé '''Ne'''on »
* « '''LiBe'''rté de '''B'''oire '''C'''ar '''N'''ous '''O'''n '''F'''oire '''N'''os '''e'''xams »
* « '''Li'''bérez '''Be'''n '''B'''arka '''C'''ar '''N'''ous, '''O'''fficiers '''F'''rançais, '''Né'''gocions »
* « '''Li'''li et '''Be'''rnard '''B'''aisent '''C'''omme '''N'''ous '''O'''n '''F'''ait '''Ne'''spa »
===== [[w:Éléments de la période 3|Période 3]] =====
''Pour : '''S'''odium, '''M'''a'''g'''nésium, '''Al'''uminium, '''Si'''licium, '''P'''hosphore, '''S'''oufre, '''C'''h'''l'''ore, '''Ar'''gon.''
* « '''S'''uzanne '''M'''an'''g'''ea '''Al'''lègrement '''Si'''x '''P'''russiens '''S'''ans '''Cl'''aquer '''A'''p'''r'''ès. »
* « '''S'''uzanne '''M'''an'''g'''ea '''Al'''lègrement '''Si'''x '''P'''oulets '''S'''ans '''Cl'''aquer des '''Ar'''ticulations. »
* « '''S'''uzanne '''M'''an'''g'''ea '''Al'''lègrement '''Si'''x '''P'''oulets (ou '''P'''erdrix) '''S'''ans '''Cl'''aquer d' '''Ar'''gent. »
Le sodium est représenté par '''Na'''. Alors Napoléon remplace Suzanne pour retrouver le symbole :
* « '''Na'''poléon '''M'''an'''g'''ea '''Al'''lègrement '''Si'''x '''P'''oulets '''S'''ans '''Cl'''aquer d''''Ar'''gent » — ou « sans claquer '''A'''p'''r'''ès », « d''''Ar'''gon », « d''''Ar'''tère », « (d')'''Ar'''tiche » ou « sans claquer les '''Ar'''ticulations » pour éviter la confusion avec l'élément argent, noté '''Ag'''.
* « '''Na'''poléon '''M'''an'''g'''ea '''Al'''lègrement '''Si'''x '''P'''russiens '''S'''ans '''Cl'''ore l’'''Ar'''mistice. »
* « '''Na'''guère '''M'''onsei'''g'''neur '''Al'''louche '''Si''' '''P'''ervers '''S'''uça '''Cl'''aire '''Ar'''demment. »
* « '''Na'''poléon '''M'''an'''g'''eait '''Al'''lègrement '''Si'''x '''P'''oulets '''S'''ans '''Cl'''amser '''A'''p'''r'''ès. »
* « '''Na'''poléon '''M'''a'''g'''nera '''À''' '''l'''<nowiki/>'est '''Si''' '''P'''ossible '''S'''a '''C'''o'''l'''onne '''A'''rmée. »
*« '''Na'''billa '''M'''an'''g'''e (h)'''Al'''lal '''Si''' '''P'''atrick '''S'''ébastien '''Cl'''ash '''Ar'''thur. »
===== [[w:Éléments de la période 4|Période 4]] =====
''Pour : '''K'''allium (Potassium), '''Ca'''lcium, '''Sc'''andium, '''Ti'''tane, '''V'''anadium, '''C'''h'''r'''ome, '''M'''a'''n'''ganèse, '''Fe'''r, '''Co'''balt, '''Ni'''ckel, '''Cu'''ivre, '''Z'''i'''n'''c, '''Ga'''llium, '''Ge'''rmanium, '''A'''r'''s'''enic, '''Sé'''lénium, '''Br'''ome, '''Kr'''ypton.''
* « '''K'''arl '''Ca'''pitaine '''Sc'''andinave '''Ti'''ra sa '''V'''erge '''Cr'''asseuse et '''M'''i'''n'''uscule, '''Fé'''conda le '''Co'''n de '''Ni'''cole, et le '''Cu'''l de ses '''Z'''e'''n'''nemies, '''Ga'''rdant '''Ge'''néreusement '''As'''sez de '''Se'''mence pour ce '''Br'''ave '''K'''h'''r'''ouchtchev. »
* « '''K'''évin '''Ca'''pture un '''Sc'''arabée '''Ti'''mide dans le '''V'''agin '''Cr'''éatif de '''M'''o'''n'''ique, '''Fé''' (fait) '''Co'''mme '''Ni'''cole dans le '''Cu'''l en '''Zinc''' de '''Ga'''spard de '''Ge'''rmanie, puis '''As'''pire '''Sé'''bastien dans la '''Br'''aguette du '''Kr'''aken »
* « '''K'''hrouchtchev '''Ca'''ressa '''Sc'''iemment '''Ti'''to. '''V'''orochev '''Cr'''ia '''M'''ag'''n'''anime : "'''Fé''' pas le '''Co'''n '''Ni'''kita, ton '''Cu'''l en '''Z'''i'''n'''c '''Ga'''lvanisé te '''Gè'''ne '''AsSe'''z pour '''Br'''anler des '''Kr'''evettes. »
* « '''K'''épler '''Ca'''lculait des '''Sc'''alaires '''Ti'''tanesques, '''V'''oyant '''Cr'''o-'''M'''ag'''n'''on '''Fé'''sant le '''Co'''n '''Ni'''ché sur le '''Cu'''l d'un '''Z'''ébulo'''n''', '''Ga'''gnant '''Gé'''néralement '''AsSe'''z de '''B'''iè'''r'''es '''Kr'''onenbourg. »
* « '''K''' '''Ca''' (cacas) '''Sc'''iés de '''Ti'''ti '''V'''olant et '''Cr'''os '''M'''i'''n'''et qui '''F'''ont ('''Fe''') des '''Co'''nneries ont '''Ni'''qué le '''Cu'''l à '''Z'''a'''n'''zibar d'un '''Ga'''rs '''Gé'''nial '''As'''sis '''Se'''rrant une '''B'''onne ('''Br''') '''Kr'''o. »
* « '''K'''hrouchtchev '''Ca'''ressa '''Sc'''andaleusement '''Ti'''tov. '''Van'''ia '''Cr'''ia '''M'''ag'''n'''animement "'''F'''ais pas le '''Co'''n Nikita, la Cuisine en '''Z'''i'''n'''c de la '''Ga'''re de '''Ge'''nève '''As''' Ses '''Br'''iques '''Cr'''euses » (ou '''K'''hrouchtchev '''Ca'''ssa le '''SC'''ooter à '''TI'''tov)
* « '''K'''hrouchtchev '''Ca'''ressa '''Sc'''andaleusement la '''Ti'''gnasse de '''V'''anadia, '''Cr'''oyant '''M'''a'''n'''ifestement '''Fe'''re '''Co'''cu '''Ni'''colaiev, le '''Cu'''ré de '''Z'''a'''n'''zibar '''Ga'''gna '''Ge'''nève '''As'''sez '''Se'''crètement avec Son '''Br'''éviaire '''Kr'''ipté. »
* « '''K'''arl '''Ca'''valier '''Sc'''andinave '''Ti'''ra '''V'''engeance '''C'''r'''u'''elle '''M'''a'''n'''iant le '''Fe'''r '''Co'''ntre le '''Ni'''kel. Le '''Cu'''l de '''Z'''e'''n'''obie '''Ga'''rnit de '''Ge'''ranium '''As'''pire la '''Se'''ve '''Br'''ûlante du '''Kr'''atère (cratère/Krypton). »
* « '''K'''onrad '''Ca'''pitaine '''Sc'''andinave '''Ti'''ra sa '''V'''erge '''Cr'''asseuse et '''M'''i'''n'''uscule, '''Fe'''rmant le '''Co'''n de '''Ni'''cole, le '''Cu'''l de '''Zn'''obie, et '''Ga'''rdant '''Ge'''néreusement l''''As'''permique '''Se'''mence du '''Br'''ave '''K'''e'''r'''mit. »
* « '''K'''a'''Ca''' '''Sc'''quatte avec '''Ti'''ti la '''V'''oiture de '''Cr'''os '''M'''i'''n'''et. '''Fe'''rnand '''Co'''nduit sa mi'''Ni''' '''COOPER''' en '''Z'''i'''g'''za'''Ga'''nt, '''Ge'''néralement '''As'''sis en se '''Se'''rvant une '''B'''ière '''Kr'''onembourg. »
* « '''K'''hrouchtchev '''Ca'''ressa '''Sc'''rupuleusement le '''Ti'''tanesque et '''V'''elue '''Cr'''ane du '''M'''o'''n'''de avec '''Fe'''rmeté. '''Co'''ntre l'ennemie, '''Ni'''kita '''C'''a'''u'''sa la '''Z'''iza'''n'''ie, il en'''Ga-Ge'''a l''''As'''sault '''S'''talinien. '''Br'''avo '''K'''h'''r'''ouchtchev. »
* « '''K'''af'''Ca''' (Kafka) '''Sc'''ruta, '''Ti'''mide, '''V'''era '''Cr'''uz '''M'''o'''n'''trant ses '''Fe'''sses, '''Co'''mme la '''Ni'''mphe (nymphe) '''Cu'''pide '''Z'''é'''n'''a. '''Ga'''llien et '''Ge'''rard, '''As'''ssis, '''Se''' '''Br'''assaient de la '''Kr'''onenbourg. »
* « '''K'''orrigan '''Ca'''pitaine '''Sc'''andinave '''Ti'''rant sa '''V'''erge '''Cr'''asseuse et '''M'''i'''n'''uscule ('''Mn''') '''Fe'''rma le '''Co'''n de '''Ni'''cole et le '''Cu'''l de '''Z'''é'''n'''obie ('''Zn''') '''Ga'''rdant '''Gé'''néreusement l’'''As'''permatique '''Se'''mence d’un '''Br'''un '''Kr'''omatique (chromatique).»
*« '''K'''évin '''Ca'''sse '''Sc'''iemment sa '''Ti'''relire et '''V'''ient '''Cr'''ier : "'''M'''ama'''n''', fait ('''Fe''') '''Co'''mme '''Ni'''cole, '''Cu'''isine !". '''Z'''hi'''n'''g lui dit : "dé'''GaGe''', '''AsSe'''z '''Br'''aillé '''Kr'''étin".
* « '''K'''arine '''Ca'''lcula que '''Sc'''ientifiquement '''Ti'''tillé, '''V'''incent '''Cr'''ame '''M'''o'''n''' '''F'''outr'''e''' '''Co'''mme '''Ni'''colas '''Cu'''pide '''Z'''i'''n'''zin '''Ga'''lleux '''Ge'''sticulant '''As'''ymétriquement et '''Se''' '''Br'''ulant à la '''Kr'''yptonite. »
* « '''K'''ptain '''Ca'''ca '''Sc'''andinave '''Ti'''re sa '''V'''erge '''Cr'''asseuse et '''M'''i'''n'''uscule (Mn) des '''Fe'''sses de '''Co'''rine '''Ni'''çoise, '''Cu'''ltivée et '''Z'''e'''n''' (Zn), '''Ga'''lamment '''Ge'''néreuse, '''As'''sez '''Se'''xy et '''Br'''anlant '''K'''a'''r'''im (Kr). »
* « '''K'''évin '''Ca'''tapulta '''Sc'''iemment '''Ti'''bère le '''V'''erreux, le '''Cr'''étin, le '''M'''écha'''n'''t, '''Fe'''roce, '''Co'''rrompu. Ha'''Ni'''bal, '''Cu'''i'''Z'''a'''n'''t, '''Ga'''ve '''Ge'''ntillement d''''As'''pirine '''Se'''c '''Br'''utus le '''Kr'''asseux. »
* « '''K'''arine '''Ca'''ressa '''S'''e'''c''' '''Ti'''mothée '''V'''ers sa '''Cr'''oupe '''M'''ais '''n'''e '''Fé'''(fait) '''Co'''uille '''Ni''' '''Cu'''l. '''Z'''ho'''n'''g '''Ga'''gna '''Ge'''ntiment '''A s'''e '''Br'''anler '''Kr'''asseusement. »
* « '''K'''oalas de '''Ca'''nberra, '''S'''’é'''c'''ria-'''T'''-'''i'''l, je '''V'''eux '''Cr'''oire '''M'''o'''n''' '''F'''r'''è'''re '''Co'''mplètement ! Ils '''Ni'''chent, '''C'''op'''u'''lent en '''Z'''o'''n'''ages '''Ga'''lamment '''Gé'''rés, '''As'''sistés '''Se'''ulement de '''Br'''ouillons '''K(r)'''yptés. (variante : de '''Br'''aves '''K'''angou'''r'''ous)»
===== [[w:Éléments de la période 5|Période 5]] =====
''Pour : '''R'''u'''b'''idium, '''S'''t'''r'''ontium, '''Y'''ttrium, '''Z'''i'''r'''conium, '''N'''io'''b'''ium, '''Mo'''lybdène, '''T'''e'''c'''hnétium, '''Ru'''thénium, '''Rh'''odium, '''P'''alla'''d'''ium, '''A'''r'''g'''ent, '''C'''a'''d'''mium, '''In'''dium, '''S'''ta'''n'''num (Étain), '''S'''ti'''b'''ium (Antimoine), '''Te'''llure, '''I'''ode, '''Xé'''non.''
* « '''R'''o'''b'''in '''S'''u'''r''' '''Y'''vette a le '''Z'''èb'''r'''e '''N'''o'''b'''le de '''Mo'''nsieur '''T'''u'''c''' '''Ru'''. '''R'''o'''h'''an '''P'''ru'''d'''emment '''Ag'''é '''C'''é'''d'''a '''In'''évitablement '''S'''a'''n'''s '''S'''u'''b'''ir '''Te'''s '''I'''dées '''Xé'''nophobes. » (variante : '''R'''o'''b'''in '''S'''o'''r'''t '''Y'''von, le '''Z'''èb'''r'''e '''N'''o'''b'''le, dans '''Mo'''n '''T'''a'''c'''ot '''R'''o'''u'''illé)
* «'''R'''o'''b'''ert '''S'''enio'''r''' est un '''Y'''éti du '''Z'''aï'''r'''e ou un '''N'''o'''b'''lio de '''Mo'''dène. Le '''T'''e'''c'''hnicien '''Ru'''dement bourré au '''Rh'''um '''P'''é'''d'''ale '''Ag'''ilement des '''C'''ou'''d'''es, il '''I'''nsulte '''S'''ai'''n'''t '''S'''e'''b'''astien, un '''Te'''rrien '''I'''diot et '''Xé'''nophobe. »
* « '''R'''u'''b'''y, '''S'''o'''r'''te de '''Y'''éti '''Z'''aï'''r'''ois qu’un '''N'''o'''b'''liau de '''Mo'''dène, '''T'''e'''c'''hniquement '''Ru'''iné, '''R'''ac'''h'''ète au '''P'''ala'''d'''in avec l’'''A'''r'''g'''ent des '''C'''a'''d'''eaux '''In'''digènes '''S'''a'''n'''s '''S'''u'''b'''ir '''Te'''s '''I'''res '''Xé'''nophobes. »
* « Le '''R'''a'''b'''bin '''S'''o'''r'''t son '''Y'''acht, le '''Z'''èb'''r'''e, pendant que '''N'''a'''b'''il, le '''Mo'''ldave '''T'''ur'''c''' '''Ru'''dement bourré au '''Rh'''um '''P'''é'''d'''ale '''Ag'''ilement des '''C'''ou'''d'''es, '''In'''diquant à '''S'''o'''n''' ami '''S'''é'''b'''astien la '''Te'''rre '''I'''mbibée de '''Xé'''rès. »
* « '''R'''o'''b'''in '''S'''′'''r'''approche du '''Y'''éti sur un '''Z'''èb'''r'''e, '''No'''nobstant le '''Mo'''rse '''Tc'''hèque en '''Ru'''t et le '''Rh'''inocéros '''P'''é'''d'''é '''Ag'''ressif et '''C'''an'''d'''ide ; '''In'''capable de '''Sn'''iffer du '''S'''a'''b'''le et de la '''Te'''rre en '''I'''mitant '''Xé'''na. »
* «'''R'''o'''b'''ert '''S'''enio'''r''', un '''Y'''éti du '''Z'''aï'''r'''e, '''N'''o'''b'''le et '''Mo'''rose, '''T'''ri'''c'''otait '''Ru'''e du '''Rh'''um et, '''P'''en'''d'''ant l′'''Ag'''enouillement du '''C'''i'''d''', '''In'''sulta '''Sn'''obes et '''Sb'''ires '''Te'''rrorisés à l′'''I'''rruption d′un '''Xé'''nophobe. »
===== [[w:Éléments de la période 6|Période 6]] =====
''Pour : '''C'''é'''s'''ium, '''Ba'''ryum, '''La'''nthane, '''H'''a'''f'''nium, '''Ta'''ntale, '''W'''olfram (Tungstène), '''R'''h'''é'''nium, '''Os'''mium, '''Ir'''idium, '''P'''la'''t'''ine, '''Au'''rum (Or), '''H'''ydrar'''g'''irum (Mercure), '''T'''ha'''l'''lium, '''P'''lom'''b''', '''Bi'''smuth, '''Po'''lonium, '''A'''s'''t'''ate, '''R'''ado'''n'''.'' (La ou Lu selon la classification)
* « '''C'''é'''s'''ar '''Ba'''lade '''La''' '''H'''i'''f'''i de '''Ta'''ta dans le '''W'''agon et '''Re'''garde '''Os'''si '''Ir'''ma. '''P'''e'''t'''er '''Au''' '''H'''an'''g'''ar, un '''T'''e'''l''' '''P'''ro'''b'''lème '''Bi'''en '''Po'''sé '''At'''tend '''R'''épo'''n'''se. » (variante : et '''Re'''garde '''Os'''ciller '''Ir'''ma)
* « '''C'''é'''s'''ar '''Ba'''isa '''La'''ngoureusement l''''H'''orri'''f'''iante '''Ta'''ntouse dans les '''W'''C ('''Ré'''pugnants), '''Ré'''pétant les '''Os'''cillations '''Ir'''resistibles du '''P'''é'''t'''ard d''''Au'''rélien ; Mercure('''Hg''') lui '''T'''ai'''l'''la dans le '''P'''lom'''b''' une '''Bi'''te '''Po'''ilue pour lui '''A'''s'''t'''iquer les '''R'''ei'''n'''s. »
* « '''C'''a'''s'''imir et '''Ba'''stien '''La'''ncent des '''H'''yper'''f'''réquences qui '''Ta'''pent sur un '''W'''agon '''Re'''mpli d''''Os''' '''Ir'''radiés, qui '''P'''é'''t'''a à l''''Au'''be '''H'''y'''g'''iénique, '''T'''é'''l'''éportant un '''P'''lom'''b'''ier '''Bi'''zarre '''Po'''lonais '''At'''taché à la '''R'''ei'''n'''e. »
===== [[w:Éléments de la période 7|Période 7]] =====
Pour : '''Fr'''ancium, '''Ra'''dium, '''Ac'''tinium, '''R'''uther'''f'''ordium, '''D'''u'''b'''nium, '''S'''eabor'''g'''ium, '''B'''o'''h'''rium, '''H'''a'''s'''sium, '''M'''ei'''t'''nérium, '''D'''arm'''s'''tadtium, '''R'''oent'''g'''enium, '''C'''oper'''n'''icium
* « Les '''Fr'''ancais '''Ra'''lent '''Ac'''tivement depuis que '''R'''a'''f'''farin a '''D'''ou'''b'''lé '''S'''é'''g'''olène, é'''B'''a'''h'''ie par son '''H'''i'''s'''toire '''M'''ue'''t'''te sur la '''D'''i'''s'''tribution '''R'''é'''g'''ionale de la '''C'''o'''n'''nerie. »
* « '''Fr'''anck '''Ra'''te '''Ac'''tuellement le '''R'''on'''f'''lement '''D'''é'''b'''ile du '''S'''i'''g'''nal '''B'''o'''h'''émien à l''''H'''i'''s'''toire '''M'''y'''t'''hique... »
===== [[w:Lanthanides|Lanthanides]] =====
''Pour : ('''La'''nthane), '''Cé'''rium, '''Pr'''aséodyme, '''N'''éo'''d'''yme, '''P'''ro'''m'''ethium, '''S'''a'''m'''arium, '''Eu'''ropium, '''G'''a'''d'''olinium, '''T'''er'''b'''ium, '''Dy'''sprosium, '''Ho'''lmium, '''Er'''bium, '''T'''hulliu'''m''', '''Y'''tter'''b'''ium, '''Lu'''técium''
* « '''Cé'''dric, '''Pr'''ophète '''N'''éan'''d'''ertalien, '''P'''ro'''m'''et la '''S'''a'''m'''ba. '''Eu'''gène, '''G'''ran'''d''' '''T'''rou'''b'''le '''Dy'''namique des '''Ho'''mmes '''Er'''rants, '''T'''o'''m'''be sur l'h'''Yb'''ride '''Lu'''ne. »
* « La '''Ce'''llule du '''Pr'''ofesseur est à '''N'''otre-'''d'''ame de '''P'''ana'''m'''e,'''S'''o'''m'''met de l' '''Eu'''rope, '''G'''ran'''d'''e et '''T'''rès''' b'''elle, où '''Dy'''onisos et '''Ho'''mère '''Er'''raient sans '''T'''u'''m'''ulte. '''Y'''a'''b'''on '''Lu'''tèce! »
* « '''Ce'''cile, qui '''Pr'''atiquait le '''N'''u'''d'''isme, '''P'''ro'''m'''ettait à la '''S'''a'''m'''aritaine '''Eu'''phorique un '''G'''o'''d'''emiché en '''T'''u'''b'''e de '''Dy'''namite, '''Ho'''chet '''Er'''otique et '''T'''u'''m'''éfiant, s'''Y'''m'''b'''ole de '''Lu'''xure. »
* « '''Ce'''sar se '''Pr'''omène avec '''N'''a'''d'''ine et '''P'''a'''m'''ina, en '''S'''e'''m'''ant les '''Eu'''nuques qui '''G'''ar'''d'''aient le '''T'''éné'''b'''reux '''Dy'''lan dans un cac'''Ho'''t car il '''Er'''rait près de la '''T'''o'''m'''be d''''Yb'''-'''Lu'''"
* « '''Ce''' '''P'''a'''r'''adis que '''N'''ous '''d'''onna ('''Nd''') '''P'''ro'''m'''éthée, '''S'''e'''m'''blable à l''''Eu'''rope, nous '''G'''ar'''d'''e des '''T'''erri'''b'''les '''Dy'''sputes à l''''Ho'''rizon. Nous '''Er'''igerons une '''T'''o'''m'''be et '''Y''' '''b'''annirons '''Lu'''cifer »
===== [[w:Actinides|Actinides]] =====
''Pour : ('''Ac'''tinium), '''Th'''orium, '''Pr'''otactinium, '''U'''ranium, '''N'''e'''p'''tunium, '''P'''l'''u'''tonium, '''Am'''ericium, '''C'''uriu'''m''', '''B'''er'''k'''élium, '''C'''ali'''f'''ornium, '''E'''in'''s'''teinium, '''F'''er'''m'''ium, '''M'''en'''d'''élénium, '''No'''bélium, '''L'''aw'''r'''encium.''
* « '''Th'''éo '''Pa'''rle '''U'''niversellement mais ''' N' '''ex'''p'''rime '''P'''l'''u'''s l''''Am'''ertume '''C'''o'''m'''mune. '''B'''roo'''k''' '''C'''on'''f'''ie l''''Es'''poir de '''F'''or'''m'''er un '''M'''on'''d'''e '''No'''uveau et '''L'''ib'''r'''e. »
* « L''''Ac'''tivation''' Th'''ermique des '''Pa'''tates à l''''U'''ranium du '''N'''é'''p'''al en '''Pu'''rée '''Am'''ène, '''C'''o'''m'''me à '''B'''ang'''k'''ok, le '''C'''on'''f'''ort '''Es'''thétique d'une '''F'''a'''m'''ine un '''M'''i'''d'''i de '''No'''ël au '''L'''ibé'''r'''ia. (ou en bord de '''Lw'''oire selon les classifications) »
* « '''Th'''or '''Pa'''rtit '''U'''ne '''N'''uit '''p'''our '''P'''l'''u'''ton, '''Am'''oureux de '''C'''a'''m'''ille. '''B'''er'''k'''eley, '''C'''ali'''f'''ornia '''Es'''perait '''F'''u'''m'''er '''M'''a '''d'''ouce et '''No'''ble '''L'''au'''r'''a.»
===== [[w:Alcalin|Alcalin]]s Groupe 1 =====
''Pour : ('''H'''ydrogène, non alcalin), '''Li'''thium, '''Na'''trium (Sodium), '''K'''allium (Potassium), '''R'''u'''b'''idium, '''C'''é'''s'''ium, '''Fr'''ancium.''
* « ('''H'''eureux) dans le '''Li'''t de '''Na'''tacha, [[w:Khrouchtchev|'''K'''hrouchtchev]] '''R'''a'''b'''aissait '''C'''on'''s'''tamment son '''Fr'''oc. »
* « '''L'''’'''i'''nter'''Na'''tionale '''K'''ommuniste '''R'''e'''b'''ute les '''C'''apitali'''s'''tes '''Fr'''ançais. »
* « '''Li'''li '''N’a''' '''K''' '''R'''e'''b'''outonner '''C'''e'''s''' '''Fr'''ocs ('''Fr'''usques). »
===== [[w:Alcalino-terreux|Alcalino-terreux]] Groupe 2 =====
''Pour : '''Bé'''ryllium, '''M'''a'''g'''nésium, '''Ca'''lcium, '''S'''t'''r'''ontium, '''Ba'''ryum, '''Ra'''dium.''
* « '''Bé'''bel(mondo) '''M'''an'''g'''eait du '''Ca'''ssoulet '''S'''u'''r''' un '''Ba'''teau '''Ra'''pide. »
* « '''Bé'''bert '''M'''an'''g'''ea du '''Ca'''nard '''S'''u'''r''' un '''Ba'''teau-'''Ra'''dar. »
* « '''Bé'''ta '''M'''an'''g'''ea du '''Ca'''ca '''S'''u'''r''' le '''Ba'''r de '''Ra'''bat (Maroc). »
* « '''Bé'''atrice '''M'''an'''g'''ea une '''Ca'''rotte en '''S'''i'''r'''otant un '''Ba'''nana-split '''Ra'''vissant. »
===== Groupe 13 =====
''Pour '''B'''ore, '''Al'''uminium, '''Ga'''llium, '''In'''dium, '''T'''ha'''l'''lium.''
*"'''B'''oris '''Al'''lait '''Ga'''mbader '''In''' '''T'''ou'''l'''ouse"
===== [[w:Cristallogène|Cristallogène]]s Groupe 14 =====
''Pour : '''C'''arbone, '''S'''ilicium, '''Ge'''rmanium, '''S'''ta'''n'''num (Étain), '''P'''lom'''b'''.''
* « '''C'''es '''S'''imples '''Ge'''stes '''S'''eraie'''n'''t '''P'''ro'''b'''lématiques'''. »'''
* « '''C''' 'est '''Si''' '''Gê'''nant '''S'''a'''n'''s '''P'''u'''b'''is. »
===== [[w:Pnictogène|Pnictogène]]s Groupe 15 =====
''Pour : '''N'''itrogène (Azote), '''P'''hosphore, '''A'''r'''s'''enic, '''S'''ti'''b'''ium (Antimoine), '''Bi'''smuth.''
* « '''N'''e '''P'''as '''As'''tiquer '''S'''e'''b''' et sa '''Bi'''te. »
* « '''N'''e '''P'''as '''As'''tiquer '''S'''o'''b'''rement le '''Bi'''zuth. »
* « '''N'''e '''P'''as '''As'''tiquer le'''S b'''outs de '''Bi'''te. »
* « '''N'''e '''PAs''' '''S'''a'''b'''rer Byzance('''Bi'''). »
* " '''N'''e '''P'''as '''As'''soir '''S'''a'''b'''rina '''Bi'''zarrement"
===== [[w:Chalcogène|Chalcogène]]s Groupe 16 =====
''Pour : '''O'''xygène, '''S'''oufre, '''Sé'''lénium, '''Te'''llure, '''Po'''lonium.''
* « '''O'''live '''S'''uce le '''Se'''xe '''Te'''ndu de '''Po'''peye. »
* « '''O'''hh '''S'''uce moi le '''Se'''xe et les '''Te'''sticules '''Po'''ilus. »
* « '''O'''scar '''S'''uce '''Se'''s '''Te'''sticules '''Po'''ilus. »
* « '''O'''rgasme '''S'''ur le '''Se'''duisant '''Te'''odore '''Po'''ilu. »
* « '''OS''' '''Se'''dimentaire '''Te'''rriblement '''Po'''li. »
* « '''O'''h '''S'''acré '''Se'''igneur aux '''Te'''sticules '''Po'''lyèdriques. »
* « '''O'''h '''S'''eigneur '''Sé''' (c'est) '''Te'''llement '''Po'''urri. »
===== [[w:Halogène|Halogène]]s Groupe 17 =====
''Pour : '''F'''luor, '''C'''h'''l'''ore, '''Br'''ome, '''I'''ode, '''A'''s'''t'''ate.''
* « '''F'''ootball '''Cl'''ub de '''Br'''èles '''I'''ncapables d''''At'''taquer. »
* « '''F'''ranck et '''Cl'''aude '''Br'''outent '''I'''rène '''A''' '''t'''able. »
* « Une '''F'''issure '''Cl'''aviculaire '''Br'''isa tout '''I'''ntérêt d''''At'''taquer. »
* « '''F'''outez '''Cl'''aire, qui '''Br'''anle '''I'''saac, car elle '''At'''tend. »
* « '''F'''antastique, '''Cl'''aire '''Br'''anche '''I'''nstinctivement l''''At'''tache. »
* « '''F'''erdinand '''Cl'''aque '''Br'''utalement '''I'''rène '''A''' '''t'''erre. »
* « Les '''F'''ameuses '''Cl'''ochettes des '''Br'''ebis d''''I'''talie '''At'''tirent. »
* « Le '''F'''ranc '''Cl'''ovis '''Br'''oie d''''I'''nnombrables '''At'''omes. »
* « '''F'''élicie '''Cl'''aqua '''Br'''ian, '''I'''nnocent '''At'''tardé. »
* « '''F'''olle '''Cl'''ara '''Br'''ave l''''I'''nvincible '''At'''hena. »
===== [[w:Gaz noble|Gaz noble]]s Groupe 18 =====
''Pour : '''Hé'''lium, '''Né'''on, '''Ar'''gon, '''Kr'''ypton, '''Xé'''non, '''R'''ado'''n'''.''
* « '''He'''rcule '''Né'''gligea d’'''Ar'''racher le '''K'''o'''r'''sage de '''Xé'''na et '''R'''o'''n'''fla. »
* « '''Hé''','''Né'''ron, '''Ar'''rête de '''Kr'''âner, '''Xé'''nophobe '''R'''i'''n'''gard ! »
===== [[w:Métalloïde|Métalloïde]]s =====
''Pour : '''B'''ore, '''Si'''licium, '''Ge'''rmanium, Arsenic '''As''', Antimoine '''Sb''', '''Te'''llure et '''Po'''lonium''
*« '''B'''ob '''Si'''ffle son '''Ge'''t '''As'''sis avec '''S'''é'''b''' devant la '''Té'''lé '''Po'''lonaise. »
==== Couples acide/base ====
L’a'''c'''i'''d'''e '''c'''è'''d'''e un ou plusieurs protons tandis que la b'''a'''se c'''a'''pte un ou plusieurs protons.
==== Couples oxydant/réducteur ====
« Les électrons sont du côté de l'Occident. » (phonétiquement ''l’oxydant'')
On peut également retenir que :
**Ox Fixe, Red Cède (L'oxydant fixe des électrons, le réducteur en cède)
** l’oxyd'''ant''' est méch'''ant''' (il prend donc des électrons) ;
** le réduct'''eur''' a bon c'''œur''' (il donne donc des électrons).
**L'oxydANT gagnANT, réductEUR donnEUR
**Cap sur l'occident ! (L'oxydant '''cap'''te les électrons)
**Notons aussi que l'oXydant aXepte (accepte) les électrons.
Phrase qui marche à la fois pour les couples Acide/Base et Oxydant/Réducteur : L'Apéro gagne toujours ! ( A[cide] perd (des protons), O[xydant] gagne (des électrons) ).
'''<nowiki/>'''
==== Ordre de priorité des groupements radicaux dans la nomenclature====
'''<nowiki/>'''
'''A'''bruti '''H'''ans '''est''' l' '''ami de''' '''Nitr'''o! '''Al'''lez '''c'''hantons, L''''alcool''' '''am'''ené '''i'''ci '''e'''st '''t'''rès '''t'''errible
Acide carboxylique, Halogénure, Ester, Amide, Nitrile, Aldéhyde, Cétone, Alcool, Amine, Imine, Ether, Thiol, Thioléter (pour ces deux derniers se reporter à la longueur).
'''A Carbalo''' '''Ester''' '''a mit''' du '''Nitrile Aldéhyde''', '''s'étonne''' '''Alcolaminimine'''. '''Et tertio''', du '''thioleter'''.
"L'''oïc''' '''est''' '''l’ami de Dalton":''' acide carboxylique (-oïque), ester, amide, aldéhyde, cétone.
(ne pas confondre l'aldéhyde et l'alcool- voir la longueur des mots: c'est le plus long qui gagne).
'''<u>Règle de Cahn, Ingold et Prelog</u>'''
<u>''pour '''I '''> '''Br''' > '''Cl''' > '''S''' > '''F '''> '''O''' > '''N''' > '''C''' > '''H'''''</u>
** « '''Ib'''ra '''Cl'''ame '''S'''a '''F'''oi '''O''' '''N'''ouveau '''C'''avani '''H'''éroïque. »
=== Thermodynamique ===
==== Loi des gaz parfaits ====
'''pV''' = '''nRT'''
'''p''' = pression en pascals ; '''V''' = volume en mètres cubes ; '''n''' = quantité de matière en mols ; '''R''' = constante des gaz parfaits. R = 8,3 J.K-1.mol-1 ; '''T''' = température en Kelvins
'''P'''ascal '''v'''oulut '''n'''ous '''r'''endre '''t'''héiste (référence au pari de Pascal)
'''P'''a'''v'''a'''n'''e'''r'''ai'''t''' (sans les voyelles)
'''pV''' = '''nRT''' n’est pas pété, énervé (ptnrv)
'''P'''rocès-'''v'''erbal ; '''n'''ous '''r'''end '''t'''riste
'''P'''uissance de '''V'''itesse = '''n'''otion de '''R'''apidité '''T'''errestre - Pour les joueurs de jeu de rôle uniquement !
Les PV d'un Pokemon est égale au Niveau fois sa RésisTance
<br>
==== Différentielle de l’enthalpie ====
dH = TdS + VdP
'''d'''îners '''H'''onorables = '''T'''artes '''d'''e '''S'''aison + '''V'''ins '''d'''u '''P'''ays (dH=TdS + VdP)
'''d'''ouces '''H'''armonies = '''T'''oniques '''d'''e '''S'''olfège + '''V'''ibrations '''d'''e '''P'''iano (dH=TdS + VdP)
'''d'''écouvertes '''H'''éroïques = '''T'''résors '''d'''e '''S'''able + '''V'''oyages '''d'''e '''P'''irates (dH=TdS + VdP)
'''d'''anses '''H'''ispaniques = '''T'''angos '''d'''e '''S'''eville + '''V'''alses '''d'''e '''P'''ampelune (dH=TdS + VdP)
'''d'''épart '''H'''éroïque = '''T'''oujours '''d'''u '''S'''tyle + '''V'''itesse '''d'''e '''P'''ointe (dH=TdS + VdP)
'''d'''estination des '''H'''istoriens = '''T'''raversant '''d'''es '''S'''iècles + '''V'''oyageant '''d'''ans le '''P'''assé (dH=TdS + VdP)
==== Différentielle de l’enthalpie libre ====
dG = VdP-SdT
'''V'''iande '''d'''e '''P'''orc '''-''' '''S'''el '''d'''e '''T'''able (VdP-SdT)
'''V'''ends '''d'''u '''P'''ain sans (-) '''S'''ortir '''d'''e '''T'''on '''G'''îte (VdP - SdT = dG)
==== Différentielle de l’énergie interne (sans variation de quantité de matière) ====
dU=TdS-PdV
'''T'''u '''d'''ois '''S'''avoir mais '''P'''as '''d'''e'''V'''iner (TdS-PdV)
'''T'''out '''d'''e '''S'''uite '''Moins''' de '''P'''oints '''d'''e '''V'''ie
'''T'''éter '''D'''u '''S'''el '''P'''endant '''D'''eux (ou '''D'''ix) '''V'''endredis.
'''T'''rou '''d'''ans le '''S'''lip et '''P'''antalon '''d'''ans le '''V'''ent
'''<nowiki>d'</nowiki>'''après '''U'''lysse = '''T'''outes '''d'''es '''S'''irènes mais '''P'''as '''d'''es '''V'''ampires
<br />
==== Différentielle de l’énergie interne (avec variation de quantité de matière) ====
dU=TdS-PdV+µdn
'''T'''rop '''d'''e '''S'''avoir mais '''P'''as '''d'''<nowiki/>'en'''V'''ie c'est être '''nu''' '''d'''ans la '''n'''uit
=== Géologie ===
'''Les ères géologiques, du Quaternaire au Primaire (permettant de retenir les datations approximatives 7x60 + 3x40 Ma)'''
'''Cénozoïque''' 60 (Quaternaire + Tertiaire) '''Crétacé''' 120 '''Jurassique''' 180 '''Trias''' 240 '''Permien''' 300 '''Carbonifère''' 360 '''Dévonien''' 420 '''Silurien''' 460 '''Ordovicien''' 500 '''Cambrien''' 540
'''C'''ite '''C'''e '''J'''oli '''T'''ruc '''P'''our '''C'''onnaître '''D'''es '''S'''iècles '''O'''rdonnés '''C'''orrectement
==== Niveaux de l'échelle chronologique géologique ====
'''Éo'''le '''ér'''adiqua les '''pe'''upliers '''ép'''uisés par l''''âge'''.
* (éon, ère, période, époque, âge)
==== Les six périodes géologiques de l’ère primaire ====
;Cambrien, Ordovicien, Silurien, Dévonien, Carbonifère, Permien.
* '''''Cambr'''onne, l’'''ord'''urier,''' s’il eû'''t été '''dévo'''t, n’eût point '''carboni'''sé son '''pèr'''e''
* '''''Cambr'''onne '''ordo'''nna '''sil'''ence et '''dévo'''uement à ses '''car'''abiniers '''perm'''issionnaires''
* '''''Cambr'''onne '''aur'''ait, '''s'il eût''' été '''dévo'''t, '''carboni'''sé son '''pèr'''e''
* '''c-or-si-dé-ca-pé''' = Corps si décapés.
* ''Le '''ca-or-sil-dé-ca-pe''' ='' Le Cahors, il décape.
==== Les trois périodes géologiques de l’ère secondaire ====
;Trias, Jurassique, Crétacé.
* '''T'''rois '''j'''ours '''c'''hacune.
==== Les cinq périodes géologiques de l’ère tertiaire ====
;Paléocène, Éocène, Oligocène, Miocène, Pliocène.
* '''Pâl'''e '''Et o'''bscène '''Au lit''', '''Mio''' se '''Plie au'''x scènes (de l'amour)
==== Stalactites et stalagmites ====
''Les stalac'''t'''ites '''t'''ombent, les stalag'''m'''ites '''m'''ontent.''
==== Géophysique ====
Formule pour la [http://fr.wikipedia.org/wiki/Anomalie_de_Bouguer correction gravitationnelle de Bouguer]:
2*π*h*ρ*G
(G=constante gravitationnelle ρ=Masse volumique/Densité)
"deux pies hachent Roger"
2 π h ρ G
(ρ = "Rho", lettre grecque)
==== [[w:Échelle de Mohs|Échelle de Mohs]] ====
"'''T'''a '''G'''rosse '''C'''oncierge '''F'''olle d''''A'''mour '''O'''se '''Q'''uémander '''T'''es '''C'''aresses '''D'''ivines"
"'''T'''oi '''G'''rand '''C'''hevalier, '''F'''uis '''A'''vec '''O'''rdre '''Q'''uand '''T'''on '''C'''œur '''D'''éfaille"
"'''T'''rès '''G'''rand '''C'''hemin de '''F'''er '''A'''pache. '''O'''h ! '''Q'''uel '''T'''emps '''C'''e '''D'''imanche !"
"'''T'''on '''G'''igolo '''C'''onte '''F'''leurette '''A''' '''(H)O'''rtense, '''Q'''ui '''T'''e '''C'''ocufie '''D'''iablement !"
"'''T'''on '''G'''rand '''C'''ul '''F'''endu '''A''' une '''O'''uverture '''Q'''ue '''T'''u '''C'''aches '''D'''écemment"
"'''T'''on '''G'''ros '''C'''ochon '''F'''ait '''A'''ïe '''O'''uille '''Q'''uand '''T'''u '''C'''ognes '''D'''essus"
([[w:Talc|'''T'''alc]], [[w:Gypse|'''G'''ypse]], [[w:Calcite|'''C'''alcite]], [[w:Fluorite|'''F'''luorite]], [[w:Apatite|'''A'''patite]], [[w:Orthose|'''O'''rthose]], [[w:Quartz (minéral)|'''Q'''uartz]], [[w:Topaze|'''T'''opaze]], [[w:Corindon|'''C'''orindon]], [[w:Diamant|'''D'''iamant]])
=== Botanique ===
==== Distinguer les hêtres des charmes ====
Le '''charme''' d''''Adam''' est d''''être''' à '''poil'''.
ou encore: "Être à poils charme Adam"
La feuille du charme a des dents (charme d’Adam)
et la feuille du hêtre a des poils (être à poil).
==== Distinguer les principales espèces de pin ====
Les aiguilles du pin '''blanc''' sont groupées par '''5'''.
'''Blanc''' a '''5''' lettres.
Le pin '''rouge''' a des aiguilles groupées par '''2'''.
Le mot '''rouge''' a '''2''' syllabes (s'il est suivit d'un mot commençant par une consonne en versification).
Les aiguilles du pin '''noir''' sont en groupes de '''2'''.
Dans le mot '''noir''', il y a '''2''' voyelles.
==== Distinguer les sapins des épicéas ====
Les sapins ('''''A'''bies'') ont des cônes '''a'''scendants, les épicéas ('''''P'''icea'') ont des cônes '''p'''endants.
==== Distinguer les cèdres ====
Le cèdre de l’'''A'''tlas a les pointes des branches '''a'''scendantes, le cèdre de l’Himalaya (''Cedrus '''d'''eodara'') '''d'''escendantes, le cèdre du '''L'''iban horizonta'''l'''es.
==== Distinguer les platanes des érables ====
pl'''A'''t'''A'''ne : feuilles '''A'''lternes
'''É'''rable : feuilles oppos'''É'''es
==== Distinguer un Catalpa d’un Paulownia ====
P'''a'''ulowni'''a''' : deux feuilles par nœud (2 fois le a dans le nom)
C'''a'''t'''a'''lp'''a''' : trois feuilles par nœud (3 a dans son nom)
==== Distinguer les feuilles de trèfle ====
Les feuilles du trèfle blanc ont de petites dents autour, celles du trèfle rouge ont des poils autour. Dents blanches, poils rouges (et non dents rouges, poils blancs !)
==== Distinguer les feuilles de trèfles de celles des luzernes ====
Les luzernes (''Medicago'') ont des pointes (= aiguilles, les médecins font des injections) au bout des folioles.
==== Distinguer les vesces des gesses ====
gesses ('''''L'''athyrus'') : l'alignement des points d'insertion des filets des étamines forme un angle droit avec le tube des étamines → L
vesces ('''''V'''icia'') : il est oblique par rapport au tube ; on retrouve ce côté oblique dans la lettre V
==== Distinguer les knauties des scabieuses ====
'''k'''nautie : 4 ('''k'''atr’) pétales dans chaque fleur de l’inflorescence
'''s'''cabieuse : 5 ('''s'''inq) pétales par fleur de l’inflorescence et des '''s'''oies sur le réceptacle
==== Distinguer les plantules de céréales dans un champ ====
BOAS :
le '''b'''lé étant plus riche a des oreillettes, des poils et une ligule
l’'''o'''rge a des oreillettes et une ligule
l’'''a'''voine a une ligule
le '''s'''eigle étant plus pauvre, n’a plus rien
=== Zoologie ===
==== Ordre des cétacés ====
« '''C'est assez''', dit la '''baleine''', al'''ors que''' j'ai le '''dos fin''' je me '''cache à l'eau'''
**baleine, orque, dauphin, cachalot, mais la liste est très incomplète.
==== Ordre des tatous ====
Les tatous font partie de l’ordre des ''Édentés'' car : "T’as tout sauf les dents !"
=== Biologie ===
==== L'ordre hiérarchique de la classification de [[w:Taxinomie|taxinomie]] ====
Des Rats Essayent de Courir là Où Finissent les Grands Espaces (Raccourcis).<br />
DoRs EnCOre, la Famille GÈRe.<br />
Reste En Classe Ou Fais Grandes Études.<br />
Reste En Contact, Odile, Fais Gaffe, Émile ! (inspiré de la Cité de la peur, où Odile est attachée de presse et Émile tueur)<br />
Domaine, Règne, Embranchement, Classe, Ordre, Famille, Genre, Espèce, (Race).
RECOFGE: Règne, Embranchement, Classe, Ordre, Famille, Genre, Espèce
==== Les [[w:bases azotées|bases azotées]] de l’ADN ====
'''À''' '''T'''on '''G'''rand '''C'''œur.
'''A'''h '''T'''a '''G'''ueule '''C'''rétin
'''À''' '''T'''able '''G'''rand '''C'''hef
('''ATGC''' : [[w:adénine|adénine]], [[w:thymine|thymine]], [[w:guanine|guanine]], [[w:cytosine|cytosine]])
==== Intron/Exon ====
'''Int'''ron = '''Int'''rus ou '''int'''rusif, c'est la partie de nucléotide d'un gène qui est excisé de l'ARN lors de l'épissage, à l'inverse des '''exons.'''
'''<nowiki/>'''
==== La séquence nucléotidique des télomères humains ====
'''T'''ous '''t'''es '''a'''mis se '''g'''avent de '''g'''énial '''g'''uarana.
(TTAGGG)
==== Les différentes phases de la [[mitose]] ====
**le Prophète Athée (Pro Met A T)
** Je te '''ProMets''' de l''''Ana'''l au '''Telo'''
** '''ProMets''' à '''Anna''' de '''Tél'''éphoner
** '''P'''etit '''M'''ammifère '''À''' '''T'''éton
** '''P'''etit '''M'''atin '''A'''uprès de '''T'''oi
** '''P'''etit '''M'''artien '''A'''ttaque la '''T'''erre
** '''P'''etite '''M'''émé '''A''' '''T'''éléphoné
** '''P'''apa '''M'''aman '''A'''mour '''T'''oujours
** '''P'''our '''M'''on '''A'''mi '''Th'''omas
** '''P'''our '''M'''on '''A'''mour '''T'''oujours
** '''P'''our '''M'''on '''A'''nus '''T'''roué
** '''P'''etite '''M'''ademoiselle '''A'''ge '''T'''endre
** Promettante : '''Pro'''/'''met'''t/'''an'''/'''te'''
** '''PROMETANATELO'''
** ou ProMéthée est AnaTello (Se rappeler de la phrase Prométhée est un intello)
** '''P'''apa '''M'''ange '''A''' '''T'''able
** '''P'''ro des '''M''' '''A''' '''T'''hs
** '''P'''rof de '''M''' '''A''' '''T'''hs
** '''P'''ierre '''M'''angea des '''A'''nanas '''T'''ransgéniques
** '''P'''ouvoir '''M'''asculin '''A'''vant '''T'''out
** '''P'''aris-'''M'''arseille '''A''' '''T'''rotinette
** '''P'''our '''M'''émoriser : '''A'''voir '''T'''ravaillé
** '''P'''our '''M'''ieux '''A'''pprendre'''T'''out
** '''P'''ays les '''M'''oins '''avancés'''
** Le '''Pro'''f '''Met''' l' '''Âne''' devant la '''Tél'''é
('''P'''rophase, '''M'''étaphase, '''A'''naphase, '''T'''élophase)
** '''P'''auline '''M'''arche '''à''' la '''T'''équila
** TAMPI (à lire à l'envers)
** c'est PRoMEtteur An(un) inTello
** Papa Mange un Abricot Trop sucré (avec le s de sucré pour la synthèse qui suit la mitose G1 --> S --> G2)
==== Les différentes phases de la PROPHASE ====
'''Le''' '''Zi'''zi du '''Pachy'''derme a des '''Di'''mensions '''Dia'''boliques.
(Leptotène, Zygotène, Pachytène, Diplotène, et Diacinèse)
ou
Letzplin (lepto/zygo) protége (pachy) didier (diplo/diacinèse)
ou
Le Zip à Didier
ou
Le zizi n'a pas de diarrhée
ou
Le zizi poilu du doyen
ou
Le zizi du pachyderme et du diplodocus sont différents
ou
Les Zizis Peuvent Devenir Durs !
ou '''Pré'''férer '''Le''' '''Zi'''zi du '''Pachy'''derme à celui du '''Diplo'''docus '''Dia'''bétique
Pour préleptotène, leptotène, zygotène, pachytène, diplotène, diacinèse
==== Les acides aminés dits essentiels : ====
''on compte neuf acides aminés essentiels : le tryptophane, la lysine, la méthionine, la phénylalanine, la thréonine, la valine, la leucine, l'isoleucine et l'histidine''
Le (LEU) trou (THR) de l'hystérique (HIS) Lyse (LYS) fait (PHE) tripper (TRY) valentin (VAL) mais (MET) ilose (ILE) pas !
'''''Le''' '''très''' '''ly'''rique '''Tri'''stan '''fait''' '''va'''chement '''m'''archer '''Ys'''eult, quelle '''Hist'''oire !''
ou encore :
Hystérique, le très lyrique Tristan fait vachement méditer Iseult en Argentine (His)Leu-Thr-Lys-Trp-Phe-Val-Met-Iso(Arg): Histidine et Arginine seulement essentiels chez les enfants.
('''Le'''ucine, '''Thré'''onine, '''Ly'''sine, '''Try'''ptophane, '''Phé'''nylalanine, '''Va'''line, '''M'''éthionine, '''Is'''oleucine, '''Hist'''idine)
Met le dans la valise, il fait trop d'histoire avec l'argent/en argentine.
le cours d’'''hist'''oire, '''il''' '''le''' '''lit''' '''mais''' '''fait''' '''tres''' '''trivial'''
('''Hist'''idine; '''Ile''': Isoleucine, '''Leu'''cine, '''Ly'''sine, '''Mé'''thionine, '''Phé'''nylalanine '''Thré'''onine,'''Try'''ptophane,, '''Va'''line)
Dans une '''V'''(aline)'''I'''(soleucine)'''L'''(eucine), il y a des '''H'''(ystidine)'''L'''(ysine)'''M'''(ethionine) et des '''P'''(hénilalanine)'''T'''(hréonine)'''T'''(ryptophane) (Dans une ville, il y a des HLM et des PTT)
''ils le valent trop trop mes félicitations''
'''ile''' '''leu''' '''val''' '''thr'''''op'' '''tr'''''o'''''p''' '''met''' '''phe''' '''lys''' ''itations''
''Va te le mettre, Phillipe''
'''VA'''l '''TH'''r '''LE'''u '''MET''' '''TR'''p '''PH'''e '''ILE''' '''LY'''s pe
'''''Va''' '''tri'''poter '''Lys'''e mais ('''met''') fait ('''phe''') '''le''' '''tr'''ès '''iso'''lément''
''val thr lys met phe (fait) leu trp ile (iso-leucine)''
Plus simple et plus concret que tous les autres moyens mnémotechniques:
VTT MILLPH (prononcé VTT MILF) et ainsi vous obtiendrez : Valine, Thréonine, Tryptophane, Méthionine, Isoleucine, Leucine, Lysine, Phénylalanine, Histidine.
==== Les acides aminés dits apolaire (Proline polaire/apolaire comprise) ====
Valérie promet à la triste Iseult le phénix et la Glycine.
(val) (Pro/Met)(Ala)(Trp) (Ile) (Leu) (Phe) (Gly)
Glycine dévale à la pelle, il le promet trop.
==== Le [[w:cycle de Krebs|cycle de Krebs]] ====
**''Si le '''citr'''on '''iso'''<nowiki>le l'</nowiki>'''acéto'''ne, le '''succi'''nct '''succès''' '''fumera''' '''m'''oins '''haut'''''
('''citr'''ate, '''iso'''citrate, alph'''acét'''oglutarate, '''succ'''inyl CoA, '''succ'''inate, '''fumara'''te, '''ma'''late, '''o'''xaloacétate)
** Avec les initiales : "C'est ici ce samedi soir : fumette, mal-à-la-tête, oubliette."
ou encore :
** ''La '''C''' '''I''' '''A''' '''su'''specte un '''su'''spect qui '''fum'''e des '''Mal'''boros '''ox'''ydées.''
** '''O'''h '''C'''atastrophe ! '''I'''l '''Os'''e '''Ac'''tiver '''Sa''' '''Su'''per '''F'''orce '''M'''agique
==== Le [[w:Cycle de Calvin|Cycle de Calvin]] ====
**"Les '''ri'''mes '''intermédiaires''' aux '''fo'''rmes '''diffo'''rmes de '''PGAL''' '''ri'''ment."
('''ri'''bulose phosphate, '''intermédiaire''' instable qui se scinde en deux 3-'''pho'''sphoglycérate, 1,3-'''dipho'''sphoglycérate, phosphoglycéraldéhyde ('''PGAL'''), dont l'un quitte le cycle et cinq sont utilisés pour reformer le '''ri'''bulose phosphate.)
==== Les Aldohexoses ====
'''Allo'''ns, '''altr'''uiste '''gl'''acer la '''mann'''e, '''Gul'''liver '''i'''ra '''gal'''érer au '''tall'''us
('''Allo'''se; '''altr'''ose; '''gl'''ucose; '''mann'''ose, '''Gu'''lose '''i'''dose '''ga'''lactose '''ta'''lose)
==== Les protéines intervenant dans les jonctions cellulaires ====
(Attention que ces phrases ne fonctionnent pleinement que si l'on connaît <i>déjà</i> les protéines intervenant, mais que l'on a du mal à retenir lesquelles font quoi.)
- Jonction Adherens : '''Vin'''t le '''cad'''avre '''é'''quipé d''''a'''rmes '''α''' qui '''plaqu'''a le '''glo'''ussant '''ca'''valier.
-> '''Vin'''culine, '''cad'''hérine-'''E''', '''a'''ctine, '''α'''-actinine, '''plak'''o'''glo'''bine, '''ca'''ténine.
- Jonction de contact focal : '''Vin'''t la '''paix''' '''intégr'''ale; les '''a'''rmes '''α''' en '''ta'''s.
-> '''Vin'''culine, '''pax'''iline, '''intégr'''ines, '''a'''ctine, '''α'''-actinine, '''ta'''line.
- Desmosome : Tout ce qui '''colle''', plus la '''kératine'''.
-> Desmo'''coll'''ine, desmo'''glé'''ine, desmo'''plak'''ine, '''plak'''oglobine, '''plak'''ophiline, '''kératine'''.
- Jonction Gap (de communication) : Elle induit une '''connexion'''.
-> '''Connex'''ines.
- Hémidesmosomes : La '''p'''yramide de '''Khé'''ops '''d'''étruit '''intégr'''alement '''la mi'''en'''ne'''.
-> '''P'''lectine, '''ké'''ratine 5 et 14, '''d'''ystonine, '''intégr'''ine α6β4, '''laminine''' 332.
- Jonction tight, ou étanche, qui comporte des "'''kissing''' points" et dont le complexe crée une "'''zonula occludens'''" : '''Embrasser''' '''Claudine''' crée une '''occlu'''sion '''acti'''ve.
-> Claudine, occludine, actine, ZO-1.
==== Les protéines des filaments intermédiaires ====
Elles diffèrent en fonction du tissu où elles se trouvent. Attention que les moyens proposés ici servent plus à retrouver la fonction d'une protéine déjà connue qu'à retenir le nom en lui-même.
- Épithéliums : Kératines.
Facile, il suffit de réfléchir un instant pour s'apercevoir que l'épithélium est bourré de kératines (couche cornée, desmosomes, ...).
- Tissu '''C'''onjonctif : '''V'''imentines.
On retient "'''CV'''".
- Tissu '''M'''usculaire : '''D'''esmines.
On retient "'''MD'''", une abréviation fréquente en anglais pour qualifier un Docteur en Médecine (Medicinæ doctor).
- Tissu Nerveux proprement dit : Protéines des neurofilaments.
Elles n'ont donc pas de nom propre, leur nom est leur fonction : des '''protéines''' dans les '''filaments''' intermédiaires des '''neuro'''nes.
- Tissu Nerveux "de soutien" (tissu glial, donc) : Protéines fibrillaires acides gliales.
Elles n'ont pas de nom propre, le nom est la fonction : Des '''protéines''' qui génèrent des '''filaments''' ('''fibrillaires''', donc) appartenant au tissu '''glial'''. La seule chose à retenir est qu'elles sont acides.
- Noyaux : Lamines.
On peut retenir qu'elles forment la '''lamina''' nucléaire, ou encore que pour arriver au noyau d'une cellule il faut la "maltraiter", et pourquoi pas la '''laminer'''.
==Technologie==
===Électronique===
====Code couleur des résistances====
Code couleur à retenir : Noir, Marron, Rouge, Orange, Jaune, Vert, Bleu, Violet, Gris, Blanc
* Ne Mangez Rien Ou Je Vous Battrai Violemment Gros Béta.
* Ne Mangez Rien Ou Je Vous Brûle Votre Grosse Barbe.
* Ne Mangez Rien Ou Jeunez Voilà Bien Votre Grande Bêtise.
=== Informatique ===
==== RJ45 croisé ====
Broches 361782'''45'''.
==== Modèle OSI ====
Le modèle OSI divise les fonctionnalités nécessaires à la communication en sept couches :
*# '''P'''hysique,
*# '''L'''iaison,
*# '''R'''éseau,
*# '''T'''ransport,
*# '''S'''ession,
*# '''P'''résentation,
*# '''A'''pplication.
** Il faut être deux pour avoir une liaison.
** Le 4*4 est un transport.
"Félicie, OSI"
*#Séduit par son '''PHYSIQUE'''
*#et n'ayant aucune '''LIAISON''',
*#je l'ai contactée sur un '''RÉSEAU''' social.
*#Arrivé chez elle en '''TRANSPORT''' en commun,
*#suivi une '''SESSION''' de va-et-vient,
*#sans aucune forme de '''PRÉSENTATION''',
*#j'y ai mis toute mon '''APPLICATION'''.
Le lendemain, elle me recontactait...
Les mots des phrases suivantes ont des initiales identiques à celles des couches, dans l'ordre ci-dessus ( P L R T S P A ) :
*#'''P'''artout '''L'''e '''R'''oi '''T'''rouve '''S'''a '''P'''lace '''A'''ssise
*# '''P'''our '''L'''e '''R'''éseau '''T'''out '''S'''e '''P'''asse '''A'''utomatiquement
*# '''P'''ar '''L'''à, '''R'''aisonnons '''T'''ransport '''S'''ans '''P'''résenter l''''A'''pplication
*# ''Pour les amateurs du jeu d'échecs :'' '''P'''rends '''L'''a '''R'''eine '''T'''out '''S'''era '''P'''lus '''A'''gréable
*# '''P'''etit '''L'''apin '''R'''ose '''T'''rouvé à la '''S.P.A.'''
*# '''P'''our '''L'''e '''R'''epas '''T'''out '''S'''era '''P'''rêt '''À''' 7 heures (7 couches)
*# '''P'''our '''L'''e '''R'''éseau '''T'''u '''S'''eras '''P'''as '''A'''ugmenté
*# '''P'''our '''L'''a '''R'''oute, '''T'''u '''S'''uis '''P'''ierre-'''A'''lain !
*# '''P'''ourquoi '''L'''e''' R'''oux '''T'''ouche '''S'''on '''P'''énis '''A'''llongé ?
*# '''P'''our '''L'''a''' R'''etenir '''T'''oujours '''S'''e '''P'''arler '''A'''vant !
*# '''P'''ierre '''L'''ouis ''' R'''este '''T'''oujours '''S'''ans '''P'''énétration '''A'''nale !
*# '''P'''endant '''L'''es '''R'''ègles '''T'''oujours '''S'''évir '''P'''ar l''''A'''nus !
Les mots des phrases suivantes ont des initiales identiques à celles des couches, dans l'ordre inverse ( A P S T R L P ) :
*# '''A'''près '''P'''lusieurs '''S'''emaines, '''T'''out '''R'''espire '''L'''a '''P'''aix
*# '''A'''près '''P'''lusieurs '''S'''odomies, '''T'''out '''R'''ectum '''L'''âche '''u'''n '''P'''et
*# '''A'''vec '''P'''atrick '''S'''abatier, '''T'''u '''R'''amasses '''L'''e '''P'''ognon
*# L''''A'''méricain '''P'''uritain '''S'''e '''T'''itille ('''R'''arement|'''R'''égulièrement) '''L'''e '''P'''hallus
*# '''A'''pparemment '''P'''atrick '''S'''ebastien '''T'''on '''R'''ectum '''L'''aisse '''P'''erplexe
*# '''A'''h '''P'''etite '''S'''alope, '''T'''u '''R'''ecraches '''L'''a '''P'''urée
==== Classe d'adresse IP ====
En binaire, compter le nombre de 1 avant le premier 0.
* A : 0 -> 127 (+ 127) <code>0xxxxxxx</code>
* B : 128 -> 191 (+ 63) <code>10xxxxxx</code>
* C : 192 -> 223 (+ 31) <code>110xxxxx</code>
== Grammaire et orthographe ==
===<u>Les principaux mots interrogatifs</u>===
Ce moyen mnémotechnique est très utile pour les coups de téléphone où l’on doit demander des renseignements. Il faut dresser rapidement la liste des mots interrogatifs sur un papier et être sûr que l’on a des réponses à toutes les questions.
'''C’est cucu, c’est occupé !'''
*'''C'''ombien ?
*'''Q'''uoi ?
*'''Q'''ui ?
*'''C'''omment ?
*'''O'''ù ?
*'''Q'''uand ?
*'''P'''ourquoi ?
=== [[w:Conjonction de coordination|Conjonctions de coordination]] ===
''Mais où est donc Ornicar ?''
(Mais, Ou, Et, Donc, Or, Ni, Car)
Mais cette méthode est pédagogiquement discutable, car elle entretient la confusion entre ''et'' (conjonction) et ''est'' (verbe ''être'' à la troisième personne du singulier), ainsi qu'entre ''ou'' (conjonction) et ''où'' (adverbe ou pronom relatif).
Attention, ''donc'' n’est plus une conjonction de coordination, mais bien un verbe conjugué pour ''est'' et un adverbe de coordination pour ''où'' !
On peut aussi l’apprendre de cette manière afin de sortir le OU et ne pas induire de confusion dans l’esprit
Mais ! Et donc Ornicar (mais, et, donc, or, ni, car) en jouant sur la sonorité de la surprise
Au Québec, on dit aussi: ''Mais où est donc Carnior ?''
===<u>Les principales prépositions</u>===
*''Adam part pour Anvers avec cent sous sûrs, entre derrière chez Decontre''
:(À, Dans, Par, Pour, En, Vers, Avec, Sans, Sous, Sur, Entre, Derrière, Chez, De, Contre)
*''Adam part pour envers avec deux cents sous chez Parmisur. ''
:(À, Dans, Par, Pour, En, Vers, Avec, De, Sans, Sous, Chez, Parmi, Sur)
*''Adam part pour Anvers avec deux cents sous.''
*"Adam part pour Anvers avec deux cents sous chez Sur."
:( À, Dans, Par, Pour, En, Vers, Avec, De, Sans, Sous, Chez, Sur)
*''Adam part pour Anvers avec cent sous de chez surdurand.''
*"(À, Dans, Par, Pour, En, Vers, Avec, Sans, Sous, De, Chez, Sur, Durant)
*''Adam Surché part pour Anvers avec deux-cents sous''
*"(À, Dans, Sur, Chez, Par, Pour, En, Vers, Avec, Sans, Sous)
*’’ Adeudans part pour Sur sans sous chez Devant-derrière avec Avant-après-contre’’
===<u>Les pronoms relatifs</u>===
3 culs domptent ouvertement monsieur lequel,Duquel,Auquel...
''<nowiki>Qui que quoi dont où lequel duquel auquel…'</nowiki>''
===<u>Les déterminants possessifs</u>===
Au pluriel: ''Mais c'était nos voleurs !''
(mes, ses, tes, nos, vos, leurs)
===<u>Orthographe</u>===
* Mou'''r'''ir ne prend qu’un “ r ” car on ne meurt qu’une fois.
* Nou'''rr'''ir prend deux “ r ” car on se nourrit plusieurs fois.
* Cou'''r'''ir ne prend qu’un “ r ”car on manque d’air en courant,<br /> mais quand on a'''rr'''ive on prend tout l’air qu’on peut.
* L’hironde'''ll'''e prend deux “ l ” car elle vole avec ses deux ailes.
* La v'''i'''e'''i'''lle ne peut marcher qu’avec ses deux bâtons.
* A'''pp'''uyer prend deux « p » car on s’appuie mieux sur deux pattes.
* Un ba'''l'''ai prend un seul “ l ” car il n’y a qu’un manche.
* Un ba'''ll'''et prend deux “ l ” car pour danser il faut deux jambes.
* Toujour'''s''', toujours un “ s ” et à jamai'''s''', ne jamais l’oublier.
* J’a'''p'''erçois sur une jambe mais j’a'''pp'''arais sur les deux.
* Quand je mets deux "p" à apercevoir, j'aperçois une faute.
* Je n’a'''p'''erçois qu’un '''p''' à a'''p'''ercevoir (ou je m’a'''p'''erçois qu’a'''p'''ercevoir ne prend qu’un '''p''').
* Enve'''l'''oppe ne prend qu’un “ l ” car on ne met qu’une lettre dans une enveloppe. En revanche pour un vélo on a deux '''p'''neus : dé''velo'''''pp'''er, en''velo'''''pp'''er, etc.
* Cuiss'''eau''' de v'''eau'''.
* Sate'''ll'''ite prend 2 '''L''' car c’est plus pratique pour voler. (et un seul '''t''' car il ne tourne qu’autour d’une seule '''T'''erre)
* Évide'''mm'''ent prend deux '''m''' comme dans '''Papa/Maman''' (à noter : tous les adjectifs qui se terminent par "ent", comme "évident", prennent 2 "m" ensuite, comme "évidemment").
* Je me souviens d’une corde en rappel : On se souvient '''DE''' quelque chose, mais on se rappelle quelque chose.
* Un pa'''r'''esseux cou'''r'''onné ca'''r'''essait une ca'''r'''otte avec un air intéressé : liste de mots qui ne prennent qu’un '''r'''.
* Co'''ll'''ine a deux colonnes (2 ” l ”) et colo'''nn'''e a deux collines (2 ” n ”).
* Dé'''velopp'''er je fais du vélo avec mes deux pieds pour pédaler.
* Échapper prend deux "P" car on s'échappe mieux avec deux pieds.
* Culo'''tt'''e prend deux '''T''' car il y a deux jambes pour une culotte
* Un professeur a un seul '''F'''ront et deux '''S'''ourcils, donc un seul F, mais deux S
* Philippe : je marche (2p) mais ne vole pas (1l)
* On parle le flaman'''D''' dans les Flan'''D'''res. Le flaman'''T''' rose est un oiseau de grande '''T'''aille.
* L’am'''a'''nde pousse sur un '''a'''rbre ; l’am'''e'''nde sur un '''e'''ssuie-glace.
*Guè'''r'''e signifie "pas beaucoup", donc un seul r. Il faut au moins deux adversaires pour faire la gue'''rr'''e, donc 2 R.
* Une P'''ê'''che (melba...) / P'''ê'''cher (du poisson) / P'''é'''cher (commettre une offense) / un P'''é'''ché (originel...) : Dans la p'''ê'''che en rivière, le '''^''' représente l’hameçon et la p'''ê'''che (fruit) représente le flotteur de la canne à p'''ê'''che). Quand on confesse au prêtre un p'''é'''ché, on fait profil bas (accent aigu sur le '''é''').
* Tous les membres de la famille ont un accent grave, sauf pépé et mémé : père, mère, nièce…
* M devant Mbappé (M devant M, B et P)
* Reg versus Erg :
** Un Reg est un désert de Roches, de pieRRes
** L’ERg est un désERt de dunes
===<u>Mots avec accent circonflexe</u>===
* Une t'''a'''che c'est suffisamment sale pour ne pas avoir besoin d'en rajouter (d'accent circonflexe)
* Le chapeau de c'''i'''me est tombé dans l’ab'''î'''me. Et celui du bo'''i'''teux dans la bo'''î'''te !
* On dit chapeau ! pour la '''tâche''' accomplie et non pas chapeau ! pour la '''tache''' sur le vêtement.
* Un chien ou un chat marche sur deux paires de pa'''tt'''es. Par contre, on fait cuire des p'''ât'''es dans une casserole qu'on couvre avec le chapeau du â.
* "Traîner ses guêtres", c'est flâner.
<u>Pour les anglophones</u>, il suffit souvent de comparer le mot anglais de même racine que le mot français sur lequel on a un doute pour l'accent circonflexe. Si ce mot anglais contient un S, le mot français équivalent contient souvent un accent circonflexe. Exemples :
* Ancêtre / Ancestor
* Apôtre / Apostle
* Arrêt / Arrest
* Bâtard / Bastard
* Bête / Beast
* Boîte / Box
* Château / Castle
*Cloître / Cloister
* Côte (anatomie, rivage, pente, culinaire) / Coast (rivage)
* Coût / Cost
* Crête / Crest (vague, cimier, huppe…)
* Dégoût / Disgust
* Épitre / Epistle
* Fête, Festif (fr) / Feast (eng)
* Guêpe / Wasp (eng) tous deux venant de vespa (latin)
* Forêt / Forest
* Hâtif / Hasty
* Hôpital / Hospital
* Hôte, hôtesse / Host, hostess
* Hâte / Haste
* Honnête / Honest
* Huître / Oyster
* Île / Island
* Intérêt / Interest
* Maître / Master
* Mât / Mast (bateau)
* Paître / To pasture
* Pâtisserie, Pâte / Pastry / Pasta (ital.)
* Plâtre / Plaster
* Quête / Quest
* Rôtir / To roast
* Tâche (travail et non salissure) / Task
* Tempête / Tempest
===<u>Pluriel</u>===
*Pluriel en OUX au lieu de OUS
::Un '''hibou''' moche comme un '''pou'''
::Avait pour '''joujou''' sur ses '''genoux'''
::Un '''caillou''' aussi '''chou''' qu’un '''bijou'''.
Variante :
::Viens mon '''chou''', mon '''bijou'''
::Viens sur mes '''genoux'''
::Avec des '''joujoux''' et des '''cailloux'''
::Pour éloigner ces vilains '''hiboux''' pleins de '''poux'''
Variante :
::Viens mon '''chou''', mon '''joujou''', mon '''bijou'''
::Sur mes '''genoux'''
::Jeter des '''cailloux'''
::À ces vieux '''hiboux''', pleins de '''poux'''
Variante :<blockquote>Viens mon '''chou''', sur mes '''genoux''' avec tes '''joujoux''' et tes '''bijoux'''</blockquote><blockquote>Pour jeter des '''cailloux''' sur les vilains '''hiboux''' pleins de '''poux'''.</blockquote>
Variante :
Répéter plusieurs fois très vite : Hi-ge-jou-bi-ca-chou-pou.
Vous avez ainsi les premières syllabes des 7 noms qui se terminent en "oux" au pluriel.
2e variante:
'''J'''e '''P'''eux '''B'''oire '''C'''omme '''C'''es '''G'''ros '''H'''ommes.
* Les noms terminés en « -al » font leur pluriel en « -aux » (sauf ''aval, bal, cal, carnaval, chacal, choral, festival, mistral, naval, pal, récital, régal''… qui font leur pluriel en « s ») :
::Dans mon pays '''natal'''
::Où les gens sont pourtant '''joviaux'''
::Eut lieu, c’était '''fatal''', un combat '''naval''',
::Heureusement, ce fut le combat '''final'''
::Parce qu’il faisait '''glacial'''.
== [[w:Grammaire|Langues étrangères]] ==
Ces langues nous sont étrangères, d’où l’importance de trouver des moyens mnémotechniques
=== [[w:Allemand|Allemand]] ===
==== Liste des particules verbales non détachables ====
''J’ai mis Cerbère en enfer'' : ge-, miss-, zer-, be-, er-, ent-, emp-, ver-
''Cerbère gémit en enfer'' : zer-, be-, er-, ge-, miss-, ent-, emp-, ver-
''Miss Verzer bégaie en panthère'' : miss-, ver-, zer-, be-, ge-, emp-, ent-, er-
==== Genre des mots ====
Les mots (de plus d'une syllabe) se terminant en -e, -ei, -ie, -heit, -keit, -tion, -ung sont féminins. <br />
Il existe bien sûr des exceptions : der Däne, das Genie, der Ursprung, der Hochsprung...
=== [[w:Anglais|Anglais]] ===
==== Mots contraires ou confondables ====
* Left: gauche / '''R'''ight: d'''r'''oite
** avec la''' main gauche''', on peut former un '''L''' en tenant les doigts en haut et le pouce en avant. C’est le '''L''' de '''L'''eft.
** dans l’alphabet le '''L''' est à '''gauche''' ('''L'''eft) et le '''R''' est à '''droite''' ('''R'''ight) :
**:A B C D E F G H I J K '''L''' M N O P Q '''R''' S T U V W X Y Z
** Copy'''right''' veut dire '''droit''' d'auteur.
** Quand on est a'''droit''', c’est bien (= '''right''' en anglais)
* Odd (3 lettres) : impair / Even (4 lettres) : pair
** Tuesday : mardi / Thu'''r'''sday : jeudi
*** Thu'''r'''sday est le quat'''r'''ième jour de la semaine, il a donc un '''r''' ('''quatrième''' lettre)
*** En classant les mots dans l’ordre lexicographique :
****jeudi (Thursday) est avant mardi (Tuesday),
****Thursday (jeudi) est avant Tuesday (mardi).
*** Je'''u'''di et Th'''u'''rsday ont tous les deux la lettre '''u''' en troisième position.
*** étymologiquement : Thursday = jour de '''Thor''', et jeudi = jour de '''Jupiter''' (Jovis die). Thor (mythologie nordique) et Jupiter (mythologie romaine) sont tous les deux ''dieu du tonnerre''. Même chose pour l'étymologie de Tuesday (jour de '''Tyr''') et de mardi (jour de '''Mars'''), tous les deux étant ''dieu de la guerre''. Mais il est plus difficile de retrouver Odin dans Tuesday.
***TUEsday sonne comme two-sday two étant égal au nombre 2 et mardi est le deuxième jour de la semaine.
=== [[w:Espagnol|Espagnol]] ===
==== [[w:Consonne|Consonnes]] doublées ====
Les seules consonnes que l’on peut trouver à l’écrit en double sont celles du mot CaRoLiNa.
On peut remarquer que le "[[w:LL|ll]]" est une consonne à part entière.
Attention ! Ne pas confondre N et Ñ
==== <u>Subjonctif</u> ====
===== verbe Savoir (saber) =====
si tu '''sé '''ton présent mais que tu ne '''sepa '''ton subjonctif ce n'est pas grave!
=== [[w:Japonais|Japonais]] ===
{{article détaillé|Japonais/Hiragana/Leçon 1}}
=== [[w:Latin|Latin]] ===
==== Ordre des six cas principaux du latin ====
'''No'''us '''Vo'''us '''Ac'''hetons '''Gé'''néralement '''D'''es '''Ab'''ricots
'''No'''minatif, '''Vo'''catif, '''Ac'''cusatif, '''Gé'''nitif, '''D'''atif, '''Ab'''latif
=== [[w:Néerlandais|Néerlandais]] ===
==== Liste des particules verbales non détachables ====
''begeherontverer' : be-, ge-, her-, ont-, ver-, er-''
BEnoit et Gerard ONT HERité du VERgER
==== Conjugaison de l’[[w:imparfait|imparfait]] ====
On forme l’[[w:imparfait|imparfait]] avec un '''t''' si le radical (Verbe -EN) du verbe se termine par F, K, P, S, T, CH.<br>
Retenez : '''F'''ran'''K'''lin '''p'''rend '''s'''on '''t'''hé '''ch'''aud. <br>
Si le radical se termine par une autre lettre, on forme l’imparfait avec un '''d'''.
Exemples : <br>
- pakken (''prendre'') : pak'''k'''-en > hij pak'''t'''e (''il prenait'')<br>
- ruilen (''échanger'') : rui'''l'''-en > hij ruil'''d'''e (''il échangeait'')
=== [[w:Russe|Russe]]===
==== Verbes à voyelle alternante ы/о dans le thème ====
'''К'''арл '''Р'''о'''М'''а'''Н'''о'''В'''
крыть « couvrir » - рыть « creuser » - мыть « laver » - ныть « gémir; faire mal » - выть « hurler »
Se fléchissent tous sur le modèle :
* infinitif : мыть (accent stable au passé: м'ыла)
* conjugaison : мóю, мóешь... мóют
Contrairement à ст'ыть, ст'ыну « refroidir » ; слыть, слывý « être réputé... »
== Littérature ==
=== Auteurs français du {{XVIIe siècle}} ===
''Sur une racine de la bruyère, une corneille boit l’eau de la fontaine Molière''
([[w:Jean Racine|Racine]], [[w:Jean de La Bruyère|Jean de La Bruyère]], [[w:Pierre Corneille|Pierre Corneille]], [[w:Nicolas Boileau|Nicolas Boileau]], [[w:Jean de La Fontaine|Jean de La Fontaine]], [[w:Molière|Molière]])
Variante : La Corneille perchée sur la Racine de La Bruyère, Boileau de La Fontaine Molière
''(Remarque : La fontaine Molière est une fontaine à Paris)''
== Théâtre ==
Face à la scène, le côté '''j'''ardin et le côté '''c'''our sont du côté de chaque initiale de '''J'''ésus '''C'''hrist, de '''J'''ules '''C'''ésar , de '''J'''acques '''C'''artier ou de '''J'''acques '''C'''hirac (J.C. : jardin à gauche, cour à droite)
Face au public, c’est l’inverse, et le côté '''cour''' est le côté du [[w:cœur|cœur]], à gauche.
== Musique ==
'''Ah ! Lala !'''
* Se souvenir de cette interjection pour faire correspondre les notes musicales latines (do, ré, mi fa...) avec les anglo-saxonnes (C, D, E, F...) '''A''' correspond à '''la''', il n'y a plus qu'à suivre B=si, C=do, D=ré, E=mi, F=fa, G=sol.
'''TS MS DSS'''
*''Nom des degrés'': '''t'''onique, '''s'''us-tonique, '''m'''édiante, '''s'''ous-dominante, '''d'''ominante, '''s'''us-dominante, '''s'''ensible
'''Sa mère la racaille ! Saleté de fumier !'''
*''Ordre des bémols'' : '''S'''i '''m'''i '''l'''a '''r'''é '''s'''ol '''d'''o '''f'''a
'''Six mille laquais repus songent au dodo, fatigués'''
*''Ordre des bémols'' : '''Si''' '''mi''' '''la''' '''ré''' '''so'''l '''do''' '''fa'''
'''Facteur, donne au soldat réjoui la missive'''
*''Ordre des dièses'' : '''Fa''' '''do''' '''sol''' '''ré''' '''la''' '''mi''' '''si'''
'''Dommage, la mine est cassée Do Majeur --> La mineur'''
*"Gamme relative" de Do Majeur
'''Il Doit Posséder Les Modes En Lui'''
Ionien, Dorien, Phrygien, Lydien, Mixolydien, Éolien, Locrien
== Géographie ==
=== Points cardinaux ===
==== Où est l’est ? ====
*Visualiser Strasbourg et Brest. Strasbourg est à l'est, Brest est à l'ouest
* Penser au mot '''O'''rang'''E''', sur une carte, l'Ouest est à gauche et l'Est à droite.
* Écrire "'''où est''' l''''est'''" --> ouest à gauche et est à droite (en considérant le nord en haut bien entendu)
* L’ouest est à gauche, l’est à droite (si le nord est au-dessus).
* Penser que si on regarde la France, à gauche c'est l' ''eau'' comme dans '''O'''uest et à droite c'est l' ''étranger'' comme dans '''E'''st.
* Penser qu'en France, on parle des "pays de l'est" (à droite sur la carte) et en parlant de la conquête de l'ouest on pense à l'Amérique (à gauche sur la carte)
* Penser à : Ouest, le suffixe "est" se trouve à droite, tout comme l’est. Ce qui signifie "ouest" à gauche et "est" à droite.
* Écrire ONE (1 en anglais) : Ouest-Nord-Est
* Penser au mot 'OiE': l’Ouest est à gauche comme le O et l’Est est à droite comme le E (si le Nord est en haut)
* Dans le mot ouest il y a un "u" comme dans gauche. Dans le mot est il n'y a pas de "u" comme dans droite.
* Retenir le mot NESO en tournant dans le sens des aiguilles d'une montre, car l'inverse donne la nausée (NOSE).
==== Le soleil se couche à l’est ou à l’ouest ? ====
* Penser qu’en France, on voit de beaux couchers de soleil sur nos côtes atlantiques, à l’ouest.
* Penser aussi au pays du soleil levant, le Japon, qui est bien à l’est du continent
* Ou encore : Le Soleil se l'''è'''ve à l’'''e'''st et se c'''ou'''che à l’'''ou'''est
=== Pays limitrophes de la France ===
Aime '''I''S''A''B''E''L''A''' (Aimer pour la lettre '''M''')
'''M'''onaco, '''I'''talie, '''S'''uisse, '''A'''llemagne, '''B'''elgique, '''E'''spagne, '''L'''uxembourg, '''A'''ndorre.
''MAL BAISÉ''
'''M'''onaco, '''A'''ndorre, '''L'''uxembourg, '''B'''elgique, '''A'''llemagne, '''I'''talie, '''S'''uisse, '''E'''spagne.
''AIMABLES''
'''A'''llemagne, '''I'''talie, '''M'''onaco, '''A'''ndorre, '''B'''elgique, '''L'''uxembourg, '''E'''spagne, '''S'''uisse
Avec l’océan Atlantique, la Manche et la Méditerranée en plus :
'' '''O'''h '''MA''' '''MER'''veilleuse '''BALISE''' ''
'''O'''h = '''O'''céan Atlantique '''MA'''='''MA'''nche '''MER'''= '''MER'''Méditerranée '''BALISE'''= '''B'''elgique '''A'''llemagne '''L'''uxembourg '''I'''talie '''S'''uisse '''E'''spagne
PS cette liste est valable seulement pour la France Métropolitaine car le pays avec lequel la France a la plus longue frontière est le ... Brésil ! (car la Guyane est un département français)
=== Grands lacs de l'Amérique du Nord ===
''SMHEOL'' (d'ouest en est)
'''S'''upérieur, '''M'''ichigan, '''H'''uron, '''E'''rié, '''O'''ntario (et St '''L'''aurent)
''HOLMES (élémentaire !)''
'''H'''uron, '''O'''ntario, St '''L'''aurent, '''M'''ichigan, '''E'''rié, '''S'''upérieur.
Une autre méthode, souvent enseignée dans les cours de géographie, fait appel au mot anglais ''foyers'' de la manière suivante :
''HOMES''
'''H'''uron, '''O'''ntario, '''M'''ichigan, '''E'''rié, '''S'''upérieur
=== Les 5 arrondissements de New-York (Boroughs) ===
Vous n'arrivez pas à vous souvenir des 5 arrondissements de la grande ville de New-York? En sachant qu'il est incontournable d'aller se promener dans les grandes avenues et rues sans s'arrêter dans un stand BBQ et y manger les bons hot-dogs d'un sympathique New-yorkais. Il faut dire: '''Si Man BBQ''' (Staten Island, Manhattan, Brooklyn, Bronx, Queen). Bon appétit, bonne visite!.
=== Pays baltes ===
Vous confondez les pays baltes sur la carte ? C'est tout simple, ils sont placés par ordre alphabétique du nord au sud... [[w:Estonie|Estonie]], [[w:Lettonie|Lettonie]], [[w:Lituanie|Lituanie]]
* Cela fonctionne aussi avec les appellations anglo-saxonnes : Estonia, Latvia, Lithuania et aussi avec les noms locaux : Eesti, Latvija, Lietuva.
* Pour les capitales de ces pays : Estonie [[w:Tallinn|Tallinn]], Lettonie [[w:Riga|Riga]] et Lituanie [[w:Vilnius|Vilnius]]
'''T'''rafic '''R'''outier '''V'''olumineux
=== Les tropiques ===
Les tropiques du Cancer et du Capricorne sont classés du nord au sud par ordre alphabétique.
Le capricorne coule (en bas) car il a plus de lettres, il est plus lourd. L'antarctique aussi : l'arctique flotte.
ou tropique du caNcer : N represente le Nord.
ou Capricorne sonne comme "Cap Horn" donc au Sud
=== Les pays d'Amérique Centrale ===
*Du nord au sud : [[w:Bélize|Bélize]], [[w:Guatemala|Guatemala]], [[w:Honduras|Honduras]], [[w:Salvador|Salvador]], [[w:Nicaragua|Nicaragua]], [[w:Costa Rica|Costa Rica]], [[w:Panama|Panama]].
BGHSNCP soit: '''B'''eau '''G'''arçon '''H'''abitant '''S'''alvador '''N'''ettoie et '''C'''i'''r'''e les '''P'''lanchers ou Belle Guatemalaise Habitant Salvador, Nage sur la Côte du Panama.
=== Fleuves de Russie ===
D'ouest en est, les initiales des cinq principaux fleuves de [[w:Russie|Russie]] forment le mot "VOILA" : [[w:Volga|Volga]], [[w:Ob|Ob]], [[w:Ienisseï|Ienisseï]], [[w:Léna|Léna]], [[w:Amour (fleuve)|Amour]].
L'Ob, le Ienissei et la Léna sont les trois plus grands cours d'eau de Sibérie.
=== Pays du Moyen-Orient ===
Le Qatar est une presQu'île dans la péninsule arabiQue. Le Qatar peut être vu comme comme une Crête de CoQ juchée sur l'Arabie saoudite et s'ouvrant sur le golfe arabo-persiQue.
=== Pays d'Asie Centrale (en -stan) ===
* Du Nord au Sud et de l'Ouest à l'Est
'''Kaz'''akhstan - '''Ou'''zbékistan - '''Ki'''rghizistan - '''Tu'''rkménistan - '''Ta'''djikistan - '''Af'''ghanistan - '''Pa'''kistan
Kaz Ou Ki Tu Ta Af Pa
Kazouki, tu taffes pas ?
=== Principales villes traversées par la Loire ===
* De l´Atlantique au Mont Gerbier de Jonc
'''Na'''thalie '''an'''goisse '''to'''ujours les '''bl'''ondes '''or'''iginaires de '''Nevers''', elles '''ro'''ugissent '''sa'''ns '''pu'''deur.
Nantes Angers Tours Blois Orléans Nevers Roanne Saint-Étienne Le Puy en Velay
== Histoire ==
=== Préhistoire ===
{{loupe|#Les périodes géologiques de l’ère primaire|# Les périodes géologiques de l’ère secondaire}}
==== Évolution des [[w:Homininae|homininés]] ====
Les '''Austral'''iens '''habil'''es eurent une '''érec'''tion, quand ils aperçurent, dans le '''néan'''t, des '''sapins''' gigantesques, <br /> ce qui donne, par ordre d'apparition<br />
[[w:Australopithèque|Australopithèque]], [[w:Homo habilis|Homo habilis]], [[w:Homo erectus|Homo erectus]], [[w:Homme de Néanderthal|Homme de Néanderthal]] et [[w:Homo Sapiens|Homo Sapiens]].
=== Les 7 Merveilles du monde antique ===
"'''Mostapha''' ! '''J’attends''' la '''copie''' !"
Variante: "'''Mostapha''' ! '''J’attends''' ta '''coloscopie''' !"
('''Mau'''solée d’Halicarnasse, '''Sta'''tue de Zeus à Olympie, '''Pha'''re d’Alexandrie, '''Ja'''rdins suspendus de Babylone, '''Tem'''ple d'Artémis à Éphèse, '''Co'''losse de Rhodes, '''Py'''ramides d’Égypte)
=== Les 7 rois de Rome ===
''Ronutuann' tarsertar'' (qu'on retient mieux en imaginant le paresseux boucher Ronu : "Ronu, tuant tard, sert tard")
('''Ro'''mulus, '''Nu'''ma Pompilius, '''Tu'''llus Hostilius, '''An'''cus Martius, '''Tar'''quin l’Ancien, '''Ser'''vius Tullius, '''Tar'''quin le Superbe)
=== Les 11 [[w:Liste des empereurs romains|premiers empereurs romains]], dans l’ordre de leur règne ===
''AuTiCaClauNéGalOViVesTiDo''
('''Au'''guste, '''Ti'''bère, '''Ca'''ligula, '''Clau'''de, '''Né'''ron, '''Gal'''ba, '''O'''thon, '''Vi'''tellus, '''Ves'''pasien, '''Ti'''tus, '''Do'''mitien)
Et les six suivants : ''NeTraHadAnMarCo''
(Nerva, Trajan, Hadrien, Antonin, Marc-Aurèle, Commode)
Cesautica
Clonegalo
Vivestido
CESar, AUguste, TIbere, CAligula
CLAUde, NEron, GALba, Othon
VItellus, VESpasien, TItus, Domitien
=== Les traités napoléoniens, dans l’ordre de leur signature ===
'''''CAV''''' (penser à une ''cave'') : ''Cambalu, Apresti, Viparis''
('''Cam'''po Formio, '''Bâ'''le, '''Lu'''néville, '''A'''miens, '''Pres'''bourg, '''Ti'''lsit, '''Vi'''enne, '''Paris''')
=== Les présidents de la troisième République française ===
''Thimagré Carcafauloufa Poindemidoudoule''
('''Thi'''ers, '''Ma'''c-Mahon, '''Gré'''vy, '''Car'''not, '''Ca'''simir-Perier, '''Fau'''re, '''Lou'''bet, '''Fa'''llières,
'''Poin'''caré, '''De'''schanel, '''Mi'''llerand, '''Dou'''mergue, '''Dou'''mer, '''Le'''brun)
OU ce petit poème :
Tire Mon Glaive
Car Casse-Pierre Fort
Loup Faillit Point
Dèche Mille Dômes
D'où Merle Brun
=== Les présidents de la cinquième République française ===
'''D'''es '''P'''illards '''G'''ouvernent, '''M'''ais '''C'''hacun '''S'''ubit '''H'''élas la Macronie
'''D'''ur '''P'''armesan '''G'''orgonzola '''M'''ozzarella '''Ch'''auds '''S'''ervis.
'''D'''ouce '''P'''atrie '''G'''auloise où '''M'''iaulent les '''Ch'''ats '''S'''iamois '''H'''eureux.
<math>\Longrightarrow</math>De Gaulle, Pompidou, Giscard d'Estaing, Mitterrand, Chirac, Sarkozy, Hollande.
'''D'''ouce '''P'''atrie '''G'''auloise où '''M'''iaulent les '''Ch'''ats '''S'''iamois '''H'''eureux et '''M'''alades.
<math>\Longrightarrow</math>De Gaulle, Pompidou, Giscard d'Estaing, Mitterrand, Chirac, Sarkozy, Hollande, Macron.
- Le sauveur de la France, Charles '''de Gaulle''', s’orthographie avec deux L. On peut retenir que de Gaulle a deux L, comme les deux barres de la croix de Lorraine, symbole de la France libre.
On retient que la Gaule n’a qu’un L comme le L unique dans Celtes. Car "les Gaulois" est le nom que Jules César donne aux Celtes.
=== Les présidents américains à partir de Roosevelt ===
'''R'''udy '''T'''ente '''E'''n '''K'''araté '''J'''e '''N'''ique '''F'''abienne '''C'''omme '''R'''udy '''B'''ouche '''C'''ontre '''B'''ouche '''O'''utré '''T'''errifié.
'''R'''oosevelt '''T'''rouva '''E'''léonore en '''K'''imono, '''J'''ames '''N'''e '''F'''ilma '''C'''arrément '''R'''ien, '''B'''rave '''C'''améraman '''B'''ouché et '''T'''êtu !
<math>\Longrightarrow</math>Roosevelt, Truman, Eisenhower, Kennedy, Johnson, Nixon, Ford, Carter, Reagan, Bush, Clinton, Bush, Obama, Trump.
=== Les dirigeants de l'URSS et de la Russie ===
'''L'''aissant '''S'''on '''K'''imono '''B'''leu '''À''' '''T'''rois '''G'''amins, '''E'''lle '''P'''ut '''M'''aintenir '''P'''outine.
L = Lénine, S = Staline, K = Khrouchtchev B = Brejnev, À = Andropov, T = Tchernenko, G = Gorbatchev.....la "virgule" marque la chute du communisme, et E = Eltsine, P = Poutine, M = Medvedev, P = Poutine
== Médecine ==
=== Plan des muscles complexus ===
1. Muscle semi-épineux de la tête
* Sème tranquillement 156 graines dans un carré de terre pour 71 arbres épineux.
(Le semi-épineux a pour origine les processus transverses de Th1 à TH5/Th6 et C4 à C7 et se termine sur les processus épineux de C7 ) th1)
2. Muscle longicissimus du cou
*Louons tranquillement une sainte tu (la) sauteras.
(Le muscle longicissimus du cou a pour origine les processus transverses de Th1 à Th5 et se termine sur les tubercules post de C3 à C7)
=== Les os du carpe ===
*''SSPP - TTCC'' (Initiales)
* ''Sca-Lu-Py-Pi T-T-Go-Oc'' (Phonétique)
<math>\Longrightarrow</math>('''Sca'''phoïde, (Semi-'''Lu'''naire)'''lu'''natum* , '''Py'''ramidal, '''Pi'''siforme - '''T'''rapèze, '''T'''rapézoïde, '''C'''apitatum ''', '''os '''C'''rochu ''')'''
NB : dans la nouvelle nomenclature ce n'est plus le semi-lunaire mais le LUNATUM
*On peut le voir sous un autre angle :
** PI - TRI - LU - SCA
*: (pisciforme) (triquetrum) (lunatum) (scaphoide)
** HA - CA - TRI - TRA
*: (hamatum) (capitatum) (trapézoide) (trapèze)
Ou encore prendre les consonnes de ces 2 mots :
*'''P'''é'''T'''a'''L'''e'''S''' : '''P'''isiforme - '''T'''riquetrum - '''L'''unatum - '''S'''caphoïde
* a'''TT'''a'''CH'''e : '''T'''rapèze - '''T'''rapézoïde - '''C'''apitatum - '''H'''amatum
*Trouvé par un étudiant :
*
** '''S'''a'''L'''e '''T'''e'''P'''u, '''<nowiki>T'</nowiki>'''é'''T'''ais à '''CH'''ier.
** '''S'''uce '''l'''a '''t'''rique '''P'''atrick, '''t'''u '''t'''ireras '''Ch'''arlotte
** Le '''S'''carabée à '''L'''unettes '''T'''rie ses '''P'''ièces, '''T'''out '''T'''as est un '''C'''apital ('''h''')Amassé"
** '''S'''a'''L'''u'''T''' '''P'''ierre, '''T'''<nowiki/>'é'''T'''ais '''C'''haud '''H'''ier.
=== Les muscles épicondyliens médiaux (ex-épitrochléens) du membre supérieur ===
''Grand Papa cuve et ronfle''
<math>\Longrightarrow</math>('''Grand pa'''lmaire, Petit '''pa'''lmaire, '''Cu'''bitus antérieur, '''Ron'''d pronateur, '''Flé'''chisseur commun superficiel)
Une autre phrase est proposée
"Grand Papa, Petit Papa, fléchit rondement le cul en avant"
<u>Avec la nouvelle nomenclature</u> :
''Paulo Fuck Les Filles Sans Défense Faisant Un CAprice''
''==> rond '''P'''ronateur, '''F'''léchisseur radial du carpe, '''L'''ong palmaire, '''F'''léchisseur '''S'''uperficiel des '''D'''oigts, '''Fl'''échisseur '''U'''lnaire du '''Ca'''rpe''
=== Les muscles épicondyliens latéraux (ex-épicondyliens) du membre supérieur ===
<math>\Longrightarrow</math>'''2'''ème '''Ra'''dial, '''Ext'''enseur '''commun''', '''ext'''enseur '''propre''' '''du 5'''ème doigt, '''Court Su'''pinateur, '''Cu'''bital '''Post'''érieur, '''Anconé'''
Deux rats excommuniés, expropriés du 5e ont cousu (court supinateur) le cul de la postière en cône.
<u>Avec la nouvelle nomenclature</u> :
''Charlie Rêve d'Explorer Des Etoiles, 5 Etoiles Uniques Au Sanctuaire''
''==>'' Court extenseur Radial du carpe, Extenseur commun des Doigts, Extenseur du 5ème doigt, Extenseur Ulnaire du carpe, Anconé, Supinateur
=== Les 12 paires de nerfs crâniens ===
==== Ancienne nomenclature ====
*'''''O'''h '''O'''scar, '''m'''a '''p'''etite '''t'''héière '''m'''e '''f'''ait '''à''' '''g'''rand '''p'''eine '''s'''ix '''g'''rogs''
*'''''O'''h '''O'''scar, '''m'''a '''p'''etite '''t'''hérèse '''m'''e '''f'''ait '''à''' '''g'''rand '''p'''eine '''s'''ix '''g'''osses''
<math>\Longrightarrow</math>'''O'''lfactifs, '''O'''ptiques, '''M'''oteur oculaire commun, '''p'''athétiques, '''T'''rijumeau, '''M'''oteur oculaire externe, '''F'''aciaux, '''A'''uditifs, '''G'''losso-pharyngiens, '''P'''neumogastriques, '''S'''pinaux, '''G'''rand hypoglosse.
* Nouvelle nomenclature
*'''''OL'''ivia '''OPT'''<nowiki>e pour l'</nowiki>'''OC'''éan c'est '''TRO'''p '''TRI'''<nowiki>ste d'</nowiki>'''A'''ller '''FA'''ire des '''V'''isites '''G'''avantes quand les '''VAGUES''' '''A'''<nowiki>pportent l'</nowiki>'''HYP'''nose.''
*'''O'''h '''O'''h ! '''O'''scar ! '''T'''a '''T'''héière '''A''' '''F'''ait '''V'''ingt '''G'''rands '''V'''erres '''A''' '''H'''ector.
*'' '''Ol'''ivier '''Op'''oil '''Ocul''' '''Troqu'''a '''Tri'''stement '''A'''vec '''Fa'''nny '''V'''ingt '''Gloss Par'''fums '''Va'''nille '''Accessoire'''<nowiki> d'</nowiki>'''Hy'''dratation.''
*'''''Ol'''é '''O'''<nowiki>scar d'</nowiki>'''Occ'''ident ! '''Tr'''availle ton '''Tri'''ceps, tes '''Abd'''o et tes '''F'''esses au '''WC'''. '''Gl'''isse '''vague'''ment ton '''accessoire''' et '''hip''' !''
*'''''O'''yez, '''O'''yez ! '''O'''bstinée '''T'''ortue '''T'''enace '''A''' '''F'''inalement '''V'''aincu, '''G'''rand '''V'''antard '''A''' '''H'''onte.''
*'''''O'''yé! '''O'''yé! '''O'''bstinée '''T'''ortue '''T'''enace '''A''' '''F'''inalement '''V'''aincu (la) '''G'''rande '''V'''ague '''À''' '''H'''awaii''.
*'' '''Ol'''af '''Opt'''a '''Occ'''asionellement pour le '''Tro'''quet '''T'''andis qu' '''Abd'''el '''Fa'''isait '''V'''alser '''G'''rand'''-P'''ère '''Vague'''ment '''A'''utour de l' '''Hippo'''campe. ''
*'''''O'''n '''O'''ccasion '''O'''livier '''T'''ries '''T'''o '''A'''nally '''F'''uck '''V'''arious '''G'''uys, '''V'''aginas '''A'''re '''H'''istory''.
*'''''Ol'''a! '''Op'''hélie '''O cul Tro'''p '''Tri'''pant '''A Fa'''it '''Co'''quettement '''Glo'''usser '''Va'''lentin '''A''' l'<nowiki/>'''Hypo'''drome.''
<math>\Longrightarrow</math>'''O'''lfactif, '''O'''ptique, '''O'''cculomoteur, '''T'''rochléaire, '''T'''rijumeau, '''A'''bducens, '''F'''acial, '''V'''estibulo-Cochléaire ''ou'' '''C'''ochléo-vestibulaire, '''G'''losso-Pharyngien, '''V'''ague, '''A'''ccessoire, '''H'''ypoglosse.
==== Sensitif ou moteur ====
- Un dernier pour savoir la '''composante de chaque nerfs''' :
Mots commençant par un '''S''' = sensitif, '''M''' = moteur, '''B''' = les deux (both). Ensuite les noms communs et les adjectifs sont parasympathiques (Money, brother, big, boobs).
*'''''S'''ome '''S'''ay '''M'''oney '''M'''atters, '''B'''ut '''M'''y '''B'''rother '''S'''ays : '''B'''ig '''B'''oobs '''M'''atter '''M'''ost.''
ex: Boobs = 10e nerf (Vague), B = sensitif et moteur, nom commun = parasympathique. Ceci correspond aux caractéristiques du nerf vague !
- Une autre plus rigolote et moins décente :
*'''''S'''eb '''S'''uces '''M'''oi '''M'''es '''D'''eux '''M'''amelons '''D'''e '''S'''ilicone '''D'''é-'''D'''é '''M'''e '''M'''anque''
'''S'''ahara '''S'''ablonneux (et) '''M'''er '''M'''orte, '''D'''eux '''M'''ondes '''D'''e '''S'''ilence (et) '''D'''éserts '''D'''e '''M'''ouvants '''M'''irages
'''S''' = sensitif,
'''M''' = moteur,
'''D'''= les deux.
Ainsi le 1{{er}} nerf crânien est sensitif.
Le 9{{e}} et le 10{{e}} sont à la fois sensitifs et moteurs, etc.
=== Les 15 collatérales de l’artère maxillaire ===
'''''T'''on '''m'''épris '''p'''eut '''a'''mener '''m'''a '''t'''empête '''p'''etite '''b'''iche '''t'''ant aimée. '''Un''' '''p'''etit '''c'''âlin '''p'''eut '''p'''ardonner''
<math>\Longrightarrow</math>('''T'''ympanique, '''M'''éningée moyenne, '''P'''etite méningée, '''A'''lvéolaire inférieure, '''M'''asseterine, '''T'''emporale profonde postérieure, '''P'''térygoïdienne, '''B'''uccale, '''T'''emporale profonde antérieure, '''A'''lvéolaire supérieure, '''In'''fra-orbitaire, '''P'''alatine descendante, du '''C'''anal ptérygoïdien, '''P'''térygo-palatine, '''P'''haryngienne)
(N.B. Non! l'artère pharyngienne est une branche de la carotide externe!)
les 15 branches dans l'ordre:
un '''T'''ic '''MENING'''é '''PE'''ut '''DE'''venir '''MA'''léfique, '''T'''andis qu'un '''BU'''bon '''TE'''rriblement '''AL'''gique '''P'''eu'''T''' être '''SOU'''lagé '''VI'''te '''PA'''r une '''PT'''yaline '''SP'''écifique
'''( T'''ympanique, '''MENINGE'''é moyenne, '''PE'''tite méningée, '''DE'''ntaire inférieure, '''MA'''ssétérine, '''T'''emporale profonde moyenne, '''BU'''ccale, '''TE'''mporale profonde antérieure, '''AL'''véolaire, '''PT'''érygoïdienne, '''SOU'''s orbitaire, '''VI'''dienne, '''PA'''latine descendante, '''PT'''érygopalatine, '''SP'''hénopalatine.)
=== Les branches de l'artère axillaire ===
'''TH'''éodore '''a''' '''m'''angé '''s'''on '''c'''a'''c'''a
<math>\Longrightarrow</math>'''TH'''oracique supérieure, '''A'''cromiothoracique, '''M'''ammaire Interne, '''S'''capulaire et les deux '''C'''irconflexes
Cette artère se trouve dans la région de l'aisselle et pas ailleurs.
=== Branche de l'artère carotide externe : ===
'''''T'''ous '''L'''es '''F'''rançais '''O'''nt '''A'''pplaudi le '''P'''résident '''M'''onsieur '''T'''hiers''.
<math>\Longrightarrow</math>('''T'''hyroïdienne supérieure, '''L'''inguale, '''F'''aciale, '''O'''ccipitale, '''A'''uriculaire postérieure, '''P'''haryngienne ascendante, '''M'''axillaire interne, '''T'''emporale superficielle):
=== Collatérales de la carotide externe ===
'''''T'''ire '''l'''a '''f'''icelle, '''p'''ortier ! '''O'''uvre '''à''' '''t'''on '''m'''aître '''r'''apidement''
<math>\Longrightarrow</math>('''T'''hyroïdienne supérieure, '''L'''inguale, '''F'''aciale, '''P'''haryngienne ascendante, '''O'''ccipitale, '''A'''uriculaire postérieure, '''T'''emporale superficielle, '''M'''axillaire, '''R'''ameau parotidien)
'''T'''ou'''s''' '''l'''es '''F'''rançais '''a'''cclament '''O'''bama '''p'''résident
<math>\Longrightarrow</math>('''T'''hyroïdienne '''s'''upérieure, '''L'''inguale, '''F'''aciale, '''A'''uriculaire postérieure, '''O'''ccipitale, '''P'''haryngienne ascendante)
'''S'''ome '''a'''ngry '''l'''ady '''f'''igured '''o'''ut '''p'''ost '''m'''enopausal '''s'''yndrom.
<math>\Longrightarrow</math>('''S'''uperior thyroidal, '''A'''scending pharyngeal, '''L'''ingual, '''F'''acial, '''O'''ccipital, '''P'''osterior auricular, '''M'''axillary, '''S'''uperficial temporal)
Ce dernier, bien qu'en anglais, a l'avantage d'indiquer les artères dans l'ordre ascendant.
'''T'''u '''P'''eux '''L'''a '''F'''ourrer '''O'''u l'''A''' '''M'''anger '''T'''oute.
<math>\Longrightarrow</math>('''T'''hyroïdienne supérieure, '''P'''haryngée ascendante, '''L'''inguale, '''F'''aciale, '''O'''ccipitale, '''A'''uriculaire postérieure, '''M'''axillaire, '''T'''emporale superficielle)
Ce dernier est une variante québécoise à caractère sexuel qui a également l'avantage d'indiquer les artères dans l'ordre ascendant.
=== Les collatérales de l’artère ophtalmique ===
'''''R'''emets '''l'''es '''c'''apotes '''s'''ans '''n'''ous '''f'''aire '''p'''<nowiki>erdre l'</nowiki>'''é'''rection''
<math>\Longrightarrow</math>(Centrale de la '''R'''étine, '''L'''acrymales, '''C'''iliaires, '''S'''upra-orbitaire, '''N'''asales, '''F'''rontales, '''P'''alpébrales, '''E'''htmoïdales antérieures et postérieures)
=== Les rameaux du plexus lombaire ===
'''Il''' '''hyp'''notise '''il'''lico l''''igu'''ane '''gé'''ant et '''fé'''roce et lui '''c'''oupe '''l'''ittéralement '''la''' '''cuisse''' qui '''fai'''sait '''ob'''struction '''car''' elle '''l'omb'''rageait.
<math>\Longrightarrow</math>('''il'''io-'''hyp'''ogastrique; '''il'''io-'''ingu'''inal; '''gé'''nito-'''fé'''moral; '''c'''utané '''l'''atéral de '''la''' '''cuisse'''; '''fé'''moral; '''ob'''turateur; du muscle '''car'''ré des '''lombes''')
=== Les rameaux du plexus sacré ===
'''Si''' un '''gl'''acier '''sup'''er '''inf'''idèle est '''honteux''' d’avoir '''per'''foré le '''rect'''um et '''élev'''é '''l’anus''' d’un '''cu'''isinier '''po'''urtant '''c'''onsentant, un '''curé''' '''fe'''ra un '''ju'''gement '''im'''partial en '''oub'''liant l’'''in'''acceptable.
<math>\Longrightarrow</math>('''sci'''atique; '''gl'''utéal '''sup'''érieur et '''inf'''érieur; '''honteux'''; '''pir'''iforme; '''rect'''al supérieur ; '''élév'''ateur de '''l’anus'''; '''cu'''tané '''po'''stérieur de la '''c'''uisse; '''carré''' '''fé'''moral et '''ju'''meau '''in'''férieur; '''ob'''turateur '''in'''terne)
=== Les muscles du grand trochanter ===
Mon '''g'''ars, '''troqu'''ons: Le '''Petit''' '''Pierre''' et '''les jumeaux''' '''moyen'''nant '''fesses''' à '''obtur'''er.
<math>\Longrightarrow</math>('''G'''rand '''Troch'''anter: '''petit''' fessier; '''pir'''iforme; '''jumeaux''' supérieur et inférieur; '''moyen fess'''ier; '''obtur'''ateurs internes et externes)
=== Les muscles de la patte d'oie ===
*SA GRA - Tte ("ça gratte")
*# Sartorius (ex-Couturier)
*# Gracile
*# Tendineux
**CGT
**Sartre est grat et tendre
**ça gratte
=== Les ménisques du genou ===
'''''CI'''TR'''OE'''N''
(Le ménisque en forme de '''C''' est le ménisque '''I'''nterne ; le ménisque en forme de '''O''' est le ménisque '''E'''xterne)
=== Les obturateurs pelvi-trochantériens ===
Être '''ex-empt''' d''''im-pôt'''s
(L'obturateur '''ex'''terne s'insère à la face '''ant'''érieure de l'os coxal, l'obturateur '''in'''terne s'insère à la face '''po'''stérieure de l'os coxal)
== Mythologie ==
=== Les 3 Grâces ===
'''Aglaé''' offre une u'''sine''' à Na'''thalie''' (variante : Aglaé offre Rosine à Nathalie)
** Aglaé, Euphrosine et Thalie
=== Les 9 Muses ===
Voici l'astuce des étudiants en grec à l'école pour retenir les neuf muses :
'''''Cl'''ame, '''Eu'''gène, '''ta''' '''mél'''odie, '''terr'''ible '''air''' '''pol'''onais, o'''ura'''gan '''cal'''culé''
<math>\Longrightarrow</math>('''Cl'''io, '''Eu'''terpe, '''Tha'''lie, '''Mel'''pomène, '''Ter'''psichore, '''Ér'''ato, '''Pol'''ymnie, '''Ura'''nie, '''Cal'''liope)
Calliope porte une couronne d’or, Clio une couronne de laurier, Érato une couronne de myrtes et de roses, Euterpe une couronne de fleurs, Melpomène une couronne de pampre de vigne, Polymnie une couronne de perles, Terpsichore une couronne de guirlandes, Thalie une couronne de lierre, et Uranie une couronne d'étoiles
=== Les dieux grecs ===
''Hazah Phadhadah''
<math>\Longrightarrow</math>('''H'''éra, '''A'''phrodite, '''Z'''eus, '''A'''pollon, '''H'''éphaïstos, '''P'''oséidon, '''H'''ermès, '''A'''rtémis, '''D'''ionysos, '''H'''adès, '''A'''théna, '''D'''éméter, '''A'''rès, '''H'''estia)
Notons que la plupart des dieux grecs commencent par la lettre "A" ou "H".
{| class="wikitable"
|-
| Hestia || arrêta || d'aimer || Zeus<small>:</small>
|| possédée || <small>par</small> Dionysos<small>,</small>
|| <small>elle</small> erra || <small>dans</small> Athènes</small>,</small>
|| affolée<small>,</small> || <small>et fit</small> l'apologie || <small>de l'</small>art || effarant || d'Hermes || et d'Hadès
|-
| Hestia || Arès || Demeter || Zeus || Poséidon || Dionysos
|| Héra || Athéna || Aphrodite || Apollon || Artémis || Héphaïstos || Hermes || Hades
|}
=== Les dieux romains ===
'''''J'''eune '''v'''euve '''j'''oyeuse '''c'''herche '''v'''ieux '''b'''aron '''m'''ême '''m'''alade '''a'''fin '''d'''e '''v'''ivre '''m'''ieux'' '''p'''oint
<math>\Longrightarrow</math>('''J'''unon, '''V'''énus, '''J'''upiter, '''C'''érès, '''V'''ulcain, '''B'''acchus, '''M'''ercure, '''M'''inerve, '''A'''pollon, '''D'''iane, '''V'''esta, '''M'''ars, '''P'''luton)
== Religion ==
=== Les dimanches avant Pâques ===
Les petits protestants alsaciens (et allemands aussi, sans doute) apprenaient jadis le nom des dimanches qui précédaient Pâques grâce à la phrase : « '''I'''n '''R'''echter '''O'''rdnung '''L'''ehre '''J'''esu '''P'''assion », ce qui signifie : « Apprends dans le bon ordre la passion de Jésus ». On peut aussi dire : « '''I'''n '''R'''ektors '''O'''fen '''L'''iegen '''J'''unge '''P'''almen », ce qui veut dire : « Dans le poêle du recteur se trouvent de jeunes palmes (allusion à rameaux) ». Et ils retrouvaient : '''I'''nvocavit, '''R'''eminiscere, '''O'''culi, '''L'''aetare, '''J'''udica et '''P'''almarum (Palmsonntag - dimanche des Rameaux).
=== Péchés capitaux ===
Les initiales des [[w:sept péchés capitaux|sept péchés capitaux]] sont rassemblés dans le mot « Pô glacé ». ('''p'''aresse, '''o'''rgueil, '''g'''ourmandise, '''l'''uxure, '''a'''varice, '''c'''olère et '''e'''nvie)
** Autre expression pour retenir les 7 péchés capitaux : " CE GALOP ".
(Colère, Envie, Gourmandise, Avarice, Luxure, Orgueil, Paresse)
** L' '''ENV''' ie '''EN V'''eut, lorsqu'on lui demande de travailler, la '''P'''aresse se '''P'''ousse, l' '''O'''rgueil se pense plus haut ('''O''') que les autres, la '''G'''ourmandise mange du '''G'''ateau et du '''G'''ras, la '''L'''uxure va dans un hot-'''L''', l' '''A'''varice en '''A''', la '''C'''olère '''C'''rie.
**Ou encore une phrase: '''L'''es '''G'''rands '''E'''sprits '''O'''bligent '''C'''ertainement '''A''' '''P'''enser (luxure, gourmandise, envie, orgueil, colère, avarice, paresse)
(entendu cité par Jacques Lacarrière lors d'une interview sur France-Culture)' '
**Une phrase courte et très facile à mémoriser, car elle ne fait pas appel qu'aux initiales qui demanderaient encore beaucoup d'efforts pour les identifier. Ici, l'utilisation de mots qui agissent immédiatement sur la divulgation des 7 péchés: ''''<nowiki>Par goût, Colette envie l'orgue luxueux d'Avarice'</nowiki>''' ('''Par'''esse, '''gou'''rmandise, '''colè'''re, '''envie''', '''orgue'''il, '''lux'''ure, '''avarice''')
=== Épîtres de Paul ===
Rococo Galéphicol ThèThè TimTim TiPhilHé
('''Ro'''mains, 1 '''Co'''rinthiens, 2 '''Co'''rinthiens, '''Gal'''ates, '''Éph'''esiens,
'''Phi'''lippiens, '''Col'''ossiens, 1 '''The'''ssaloniciens,
2 '''The'''ssaloniciens, 1 '''Tim'''othée, 2 '''Tim'''othée, '''Ti'''te, '''Phil'''émon, '''Hé'''breux)
=== Apôtres de Jésus ===
'''S'''ouvent '''a'''bsent, '''J'''ean '''J'''aures '''p'''erdait '''b'''êtement '''t'''oute '''m'''onnaie le '''j'''our '''J''' de '''s'''on '''j'''eûne.
'''S'''imon, '''A'''ndré, '''J'''acques, '''J'''ean, '''P'''hilippe, '''B'''arthélémy, '''T'''homas, '''M'''atthieu, '''J'''acques, '''J'''ude, '''S'''imon, '''J'''udas.
'''Si JaJa Ma embarté Juju, J'enfile To Pierre'''
'''Si'''mon, '''Ja'''cques, '''Ja'''cques, '''Ma'''tthieu, '''An'''dré, '''Barthé'''lémy, '''Ju'''de, '''Ju'''das, '''Jean''', '''Phil'''ippe, '''Tho'''mas, '''Pierre'''.
=== '''Vertus Cardinales''' ===
'''P'''rudence, '''J'''ustice, '''F'''orce et '''T'''empérance se retient avec : " '''P'''our '''J'''ésus, '''F'''ais '''T'''out ! ", les premières lettres des mots rappellent les quatre vertus cardinales.
=== Les sept sacrements catholiques ===
''BECOROM'' :
# Baptême
# Eucharistie
# Confirmation
# Onction des malades
# Réconciliation
# Ordination
# Mariage
=== Les sept dons de l'Esprit Saint ===
''P C DS FCC (paie ces déesses fichier central des chèques)''
# Sagesse
# Discernement
# Conseil
# Force
# Connaissance
# Crainte du Seigneur
# Piété
== Navigation ==
=== Marine ===
==== Bâbord/Tribord ====
'''''Bâ'''bord c’est g'''a'''uche, t'''r'''ibord, c’est d'''r'''oite.'' (si on regarde vers la partie avant du bateau)
On peut aussi le retenir avec le mot batterie (que l’on prononce généralement « BaTri »), on a Ba à gauche (comme bâbord) et Tri à droite (comme tribord) OU plus simple avec "BaTeau".
{| class="wikitable"
| Bâ
| Tri
|-
| Bâbord
| Tribord
|-
| Gauche
| Droite
|}
Variante : la seconde lettre de b'''â'''bord est la même que la seconde lettre de g'''a'''uche et la seconde lettre de t'''r'''ibord est la même que la seconde lettre de d'''r'''oite.
Variante : Babo<s>rd ga</s>uche = Babouche.
On ne retrouve qu'un A dans bAbord et gAuche et un I dans trIbord et droIte.
==== Couleur et signalisation bâbord et tribord ====
Couleur des feux de navigation d'un bateau:
{|
| Bâbord
| '''R'''ouge
|-
| Tribord
| '''V'''ert
|}
Lu de gauche à droite cela fait '''RV''' ou le prénom '''Hervé'''.
''Un marin emporte toujours Un Tricot Vert et Deux Bas Si Rouges'' = chiffres '''impairs''', '''Tri'''bord, '''Cô'''ne, '''Vert''', et chiffres '''pairs''', '''Bâ'''bord, '''Cy'''lindre, '''Rouge'''.
Ou encore : en entrant au port, on trouve les balises COniques VERTES à TRIbord, et les balise CYlindrique ROUGES à BAbord.
En [[w:aviron|aviron]], '''T.G.V''' à savoir '''T'''ribord-'''G'''auche-'''Vert''' car le rameur est dos à l'embarcation et donc tribord se situe à sa gauche.
==== Compartimentage ====
Le compartimentage est la méthode employée pour assurer la sécurité d'un navire contre les voies d'eau. Il consiste à diviser l'espace en compartiments étanches en-dessous de la ligne de flottaison. Il résulte de ces nombreux cloisonnements une difficulté à localiser, notamment sur les gros bâtiments, tel ou tel lieu précis (cabine, soute, etc.). Afin de régler cette difficulté se pratique un référencement des locaux selon une numérotation en quatres chiffres/lettres : la tranche (de la proue vers la poupe), le pont (de la surface vers le fond à partir du pont principal 0 situé immédiatement au-dessus de la ligne de flottaison), le rang (sous-section de tranche) et le bord (local dans un rang donné).
Ex. : A216 signifie que le local se trouve dans la tranche alpha, au niveau du deuxième pont inférieur (ou faux pont), au premier rang, premier local à bâbord en partant de la ligne médiane (ou de la coursive centrale) du navire.
La mémorisation de l'ordre des bords s'effctue avec la phrase mnémotechnique suivante : "j'aime les femmes en slip", le nombre de lettres de ces mots donnant la suite 1-5-3 (impairs sur tribord) 6-2-4 (pairs sur bâbord).
==== Nœuds ====
La phrase « ''Le serpent sort du trou, tourne autour de l'arbre, et rentre dans le trou'' » permet de se rappeler de la méthode pour faire un [[w:nœud de chaise|nœud de chaise]].
=== Aviation ===
Chez les pilotes d’avions utilisant le système [[w:Precision_Approach_Path_Indicator|Vasi]] pour trouver l’altitude correcte à l’atterrissage en [[w:vol à vue|vol à vue]], des panneaux rouges et blancs à côté de la piste leur fournissent de précieuses indications qui ont abouti à cette comptine :
:« ''White over white, you're high as a kite''
:''Red over white, you're right''
:''Red over red, you're dead'' »
(Blanc sur blanc : trop haut, rouge sur blanc : correct, rouge sur rouge : trop bas).
Pour récolter toutes les informations nécessaires pour compléter leur feuille de route, les pilotes aguerris utilisent la phrase :
'''''R'''''etranchez '''v'''otre '''d'''érive, '''c'''ela '''v'''ous '''d'''onne '''c'''haque '''m'''esure '''d'''u '''c'''ap '''c'''ompas.
Route vraie - X (dérive) / Cap vrai - Déclinaison / Cap magnétique - déviation / Cap compas
Pour le décollage : CC PP VV TT (ou 3T)
Compas, Conservateur de cap (ou gyro compas), Phares, Pompe, Volets, Verrière, [[w:Transpondeur|Transpondeur]], Top, (Talons au sol). Cela dépend bien sûr du type d'avion.
Pour l’observateur au sol et face à l’avion, contrôleur, mécanicien de piste, le rouge est à droite et le vert est à gauche. Très pratique la nuit, on ne voit rien d’autre que les feux.
Moyen mnémotechnique :
Quand on se sert du vin à boire, on a le "rouge" dans la main droite et le "verre" dans la main gauche.
Ou plus facile à retenir : Comme en politique, le rouge est toujours à gauche !
== Vie quotidienne ==
La vie quotidienne regorge de moyens mnémotechniques plus ou moins utiles.
=== Droite et Gauche ===
Se souvenir de sa main habile (droitier ou gaucher)
Utiliser les initiales en majuscules (G et D) : l'arrondi est du côté correspondant (à Gauche pour G), (à droite pour D).
En anglais, pour Left et Right (Gauche et Droite). On peut faire un L avec la main gauche, avec le pouce et l'index et non avec la main droite. D'où Left=gauche et right=droite.
=== Mois courts et mois longs ===
En mettant ses poings fermés côte à côte, les bosses des [[w:phalange|phalange]]s peuvent correspondre aux mois de 31 jours du [[w:calendrier|calendrier]] et les creux entre chacune aux mois de 30 jours ou moins. Et sans compter la jonction entre les mains comme un creux.
On peut aussi le faire avec une seule main (c'est mieux pour les enfants, car ça permet de suivre le décompte des mois avec un doigt de l'autre main...) : on commence sur le premier sommet pour janvier, on s'arrête sur le dernier sommet pour juillet et, pour les mois suivants, on repart en arrière en comptant de nouveau le sommet (ou bien, on recommence au sommet initial, pour bien marquer les 2 mois de 31 jours). Attention ! Juillet-Aout ont 31 Jours mais Décembre-Janvier aussi !!
=== Heure d’été, heure d’hiver ===
En été on avance d’une heure, car "'''é'''té" et "'''a'''vance" commencent par une voyelle.
En été le soleil qui passe à travers les volets nous réveille tôt car on dort une heure en moins.
En hiver on recule d’une heure, car "'''h'''iver" et "'''r'''ecule" commencent par une consonne.
En hiver on hiberne, donc on dort une heure en plus.
Le changement d’heure se faisant en avril (!) et en octobre :
* OCTOBRE finit par RE donc on REcule
* ''AVRIL commence par AV donc on AVance''
Hélas..... cette information est affichée sur de très nombreux sites.. alors que le changement annoncé en AVRIL.. est en réalité réalisé le dernier dimanche de MARS !
Comme le changement a lieu en MARS et OCTOBRE, et qu'en général on ne peut pas (ou ne doit pas) faire reculer une aiguille sur une montre analogique :
* MARS étant plus court (4 lettres), on avance de 1 heure,
* OCTOBRE étant plus long (7 lettres), on avance de 23 heures (ou 11 heures pour les montres et horloges analogiques sans date).
Pour les anglophones : Spring Forward, Fall Back
(Spring étant le Printemps et Fall l'Automne)...
=== Retrouver de tête le nom du jour de la semaine quelle que soit la date donnée ===
{{pas clair}}
Par convention, on associe les chiffres aux lettres suivantes :
<pre>
0 = S, Z
1 = T, D
2 = N, Gn
3 = M
4 = R
5 = L, Y, ill
6 = CH, J, Ge
7 = K, Qu, Gu
8 = F, Ph, V
9 = P, B
</pre>
==== N°1 - liens entre jours et lettres : ====
Lundi est le premier jour de la semaine . Donc '''Lundi = 1''' mardi = 2 ...
==== N°2 - liens entre mois et lettres ====
===== a) correspondances pour année normale =====
:
** Janvier Février Mars... deviennent
"'''S'''a'''M''' '''M'''e '''J'''e'''T'''e'''R'''a a'''G'''e'''N'''ou'''iLL'''é '''S'''on '''M'''a'''iLL'''ot"
'''S''' = Janvier ; '''M''' = Février ; '''M''' = Mars
'''J''' = Avril ; '''T''' = Mai ; '''R''' = Juin
'''g'''= Juillet ; '''n'''= Août ; '''L'''= Septembre
'''s'''= Octobre ; '''m'''= Novembre ; '''L'''= Décembre
===== b) correspondances pour année bissextile =====
"'''G'''i'''N'''o '''M'''e '''J'''e'''T'''e'''R'''a ..."
'''G''' = Janvier ; '''N''' = Février ; etc
===== N°3 - facteur en fonction du siècle =====
====== a) Avant le 4 octobre 1582, enlever 7 au siècle (''' -7''' ) ======
Année 670 : siècle 6 '''-7''' = 0
Année 1100 : siècle 11 '''-7''' = 4
Et le résultat on le transforme :
4 devient 0 , 3 devient 1 , 5 devient 6 , 2 reste 2 et inversement.
Moyen mnémotechnique (suivant la convention) :
RuSé MaTou :: 4-0 3-1
NoNNe LouChe :: 2-2 5-6
====== b) À compter du 4 Octobre 1582 ======
soit le '''4''' - '''10''' - '''1582''' , c'est à dire le jour où le '''R'''oi '''T'''hé'''S'''ée '''T'''é'''L'''é'''PH'''o'''N'''a , on enlève 4 et non 7 , autant de multiple possible :
**Année 1800 : siècle 18 - ( '''4'''x4 ) = 2
**Année 2000 : siècle 20 - ( '''4'''x5 ) = 0
La transformation donne 0-6 1-4 2-2 3-0
{| class="wikitable"
|-
| 0 || 1 || 2 || 3
|-
| '''6''' || '''4''' || '''2'''|| '''0'''
|-
| '''CH'''è... || ...'''r'''e || '''N'''iai... || '''se'''
|-
| 0-6 || 4-1 || 2-2 || 0-3
|}
==== Calcul pour le 26 septembre 1955 ====
Il y a plusieurs opérations à réaliser en se conformant aux règles citées .
**26 Sept 1955
règle 2a ::
septembre = L = ''5''
**26+''5'' = 31
règle 3b
** 31-(7x4) = '''''3'''''
**55 - (7x4) = 55-28= 27
**27 + (27/4) = 27+6 = 33
**33 - (7x4 ) = 33-28= '''''5'''''
** '''''5 + 3 ''''' =''''' 8'''''
comme il y a 7 jours dans la semaine :
*''''' 8-7 '''''= '''1'''
le 1 correspond à '''lundi''' donc le 26 septembre 1955 était un '''lundi''', toute la journée !!!
==== Sinon pour l'année cours, passée ou à venir, ====
le moyen le plus simple et le plus rapide est de diviser l'année en trimestres . Pour chaque mois, on cherche le quantième du premier dimanche par exemple .
On fait une phrase par trimestre et pour trouver la correspondance on rajoute 7 + de 1 à 6 .
Exemple, pour 2012,
{| class="wikitable"
|-
! Janvier !! Février !! Mars
|-
| '''1''' || '''5''' || '''4'''
|-
| '''D'''e || '''<nowiki>L'</nowiki>''' || ai'''R'''
|}
Donc, le premier dimanche de Janvier 2012 est le 1er janvier . Pour aller à mon Rendez vous du 3 je rajoute 2.
** Selon ma convention déjà vue , lundi=1 mardi=2 mercredi=3 jeudi=4 vendredi=5 samedi=6 et dimanche=7 .
Alors 2 correspond à mardi donc le 3 Janvier 2012 est mardi
pour aller au 18 janvier 2012 , une semaine ayant 7 jours, même sous le règne actuel, je retire autant de semaine complète que possible . Donc 18 - (2x7 ) = 18 - 14 = 4 . Surprise, le 18 janvier 2012 sera un mercredi . Soit parce que j'ai rajouté 3 jours au dimanche, soit parce que j'ai décidé une fois pour toutes que le dimanche est le premier jour de la semaine, donc le mardi le 2ème etc ...
** dans ce cas, dimanche=1 mardi=2 mercredi=4 jeudi=5 vendredi=6 samedi=7 dimanche=8-7=1
C'est selon sa préférence intellectuelle .
Je vous laisse continuer pour février et les autres trimestres.
{| class="wikitable"
|-
! janvier !! février !! mars !! avril !! mai !! juin !! juillet !! aout !! sept !! oct !! nov !! déc
|-
| 1 || 5 || 4 || 1 || 6 || 3 || 1 || 5 || 2 || 7 || 4 || 2
|-
| D || l || r || t || ch || m || d || l || n || k || r || g
|-
| de || l' || air || tu || chô || me || dans || la || nuit || qui || rè || gne
|}
=== Valeur d'un Euro en [[w:Franc français|Francs français]] ===
Selon le même principe que pour les décimales de ''π'' :
{| border="0" cellpadding="0" cellspacing="1"
|align="center"|''Chacun''
|
|align="center"|''saura''
|
|align="center"|''enfin''
|
|align="center"|''convertir''
|
|align="center"|''notre''
|
|align="center"|''monnaie''
|-
!align="center"|6
!align="center"|,
!align="center"|5
|
!align="center"|5
|
!align="center"|9
|
!align="center"|5
|
!align="center"|7
|}
=== Morse ===
Le code morse est facilement mémorisable à l’aide des codes courts et longs remplacés par des syllabes.
Le code long (-) remplacé par une syllabe en "O".
Le code court (.) remplacé par une des autres voyelles.
Ex : A = .- = Au/tO (une syllabe en A pour le . et une syllabe en O pour le -)
La liste complète est [[w:Alphabet Morse#Tableau Mn.C3.A9motechnique|Ici]]
=== Les vins ===
==== AOC de la côte de Nuits ====
Un mnémonique permettant de se rappeler des [[wikipedia:AOC|AOC]] communales de la [[wikipedia:Côte de Nuits|Côte de Nuits]], et dans l'ordre géographique en plus, par Paul Brunet, auteur du livre ''Le vin et les vins au restaurant'' : ''Messieurs, faites gaffe, mon chat vous voit noir'' (Marsannay, Fixin, Gevrey-Chambertin, Morey-Saint-Denis, Chambolle-Musigny, Vougeot, Vosne-Romanée, Nuits-Saint-Georges).
==== Nom des bouteilles de vin ====
Ce moyen mnémotechnique permet de mémoriser les principales tailles de [[w:bouteille de vin|bouteilles]] dans l'ordre croissant de contenance :
* Car de bon matin je remarquais mal sa banalité naturelle (quart, demi, bouteille, magnum, jéroboam, réhoboam, mathusalem, salmanazar, balthazar, nabuchodonosor).
Autre phrase mnémo, plus complète :
* PICARD FIT : DE BON MATIN, JE REMARQUE SA BANALITÉ, SA MATIERE SI PAUVRE.
Pour se rappeler tous les contenants de vin ou de champagne :
* PIccolo, QUARt, FIllette, DEmi-bouteille, Bouteille, MAgnum, Jeroboam, REhoboam, Mathusalem, SAlmanazar, Balthazar, Nabuchodonosor, SAlomon, Melchisédech, Souverain, Primat.
=== <u>Dresser une table</u> ===
Pour se souvenir d'où mettre la fourchette et le couteau :
Four'''<u>ch</u>'''ette à gau'''<u>ch</u>'''e, couteau à droite.
ou encore : A, B, '''C''', '''D''' ... '''C'''outeau à '''D'''roite. E, '''F''', '''G''', H ... '''F'''ourchette à '''G'''auche
== Cinéma, Bande dessinée... ==
=== Dupond et Dupont ===
Pour différencier les deux [[w:Dupond et Dupont|dupondt]] de la bande dessinée [[w:Les Aventures de Tintin et Milou|Les Aventures de Tintin et Milou]], celui à la moustache tombante comme un D est Dupond, celui à la moustache pointue comme les barres du T est Dupont.
=== Les 7 nains ===
'''A''' '''J'''ouer '''P'''resque '''S'''eul '''T'''u '''D'''eviens '''G'''rincheux
* Atchoum, Joyeux, Prof, Simplet, Timide, Dormeur, Grincheux : les 7 nains dans Blanche Neige...
=== Les Simpson ===
Pour différencier les deux sœurs de Marge, on observe les cheveux. Patty n'a pas de raie au centre et Selma a les cheveux découpés en deux blocs par une raie ou on regarde les boucles d'oreilles qui sont différentes
Sinon, regarder les boucles d'oreilles de Patty, elles sont triangulaires (P comme Pythagore).
== Sport ==
=== Escalade ===
''Pour la prochaine longueur je reste en bas, donc je me vache au plus bas.'' <br>
:En escalade, permet de savoir sur quel point d'assurage se vacher lors du relais '''réversible''' uniquement.
== Voir aussi ==
=== Article connexe ===
* [[w:Code chiffres-sons|Code chiffres-sons]]
=== Liens externes ===
* [https://jeretiens.net -JeRetiens.net- Libre recueil ayant pour objectif de rassembler tous les trucs et astuces mnémotechniques pour retenir et apprendre plus facilement.]
* [http://www.finallyover.com/categorie-1079667.html Méthodes thématiques de mémorisation]
* [http://www.echolalie.org/wiki/index.php?ListeMnemotechnique Liste mnémotechnique]
* [http://trucsmaths.free.fr/Pi.htm#poeme Le nombre pi]
* [http://www.francaisfacile.com/exercices/exercice-francais-2/exercice-francais-75628.php francaisfacile.com]
=== Références ===
<references />
[[Catégorie:minilivres]]
4zbe3co2uopas75bugv5hkmk68tic8r
Les cartes graphiques/Les unités de texture
0
67395
763400
763302
2026-04-10T15:42:24Z
Mewtow
31375
763400
wikitext
text/x-wiki
[[File:Texture mapping.png|vignette|''Texture mapping'']]
Les '''textures''' sont des images que l'on va plaquer sur la surface d'un objet, du papier peint en quelque sorte. Les cartes graphiques supportent divers formats de textures, qui indiquent comment les pixels de l'image sont stockés en mémoire : RGB, RGBA, niveaux de gris, etc. Une texture est donc composée de "pixels", comme toute image numérique. Pour bien faire la différence entre les pixels d'une texture, et les pixels de l'écran, les pixels d'une texture sont couramment appelés des ''texels''.
==Le placage de textures inverse==
Pour rappel, plaquer une texture sur un objet consiste à attribuer un texel à chaque sommet, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture.
Dans les faits, on n'utilise pas de coordonnées entières de ce type. Les coordonnées de texture sont deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. Le nom donnée à cette technique de description des coordonnées de texture s'appelle l''''''UV Mapping'''''.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Les API 3D modernes gèrent des textures en trois dimensions, ce qui ajoute une troisième coordonnée de texture notée w. Dans ce qui va suivre, nous allons passer les textures en trois dimensions sous silence. Elles ne sont pas très utilisées, la quasi-totalité des jeux vidéo et applications 3D utilisant des textures en deux dimensions. Par contre, le matériel doit gérer les textures 3D, ce qui le rend plus complexe que prévu. Il faut ajouter quelques circuits pour, de quoi gérer la troisième coordonnée de texture, etc.
Lors de la rastérisation, chaque fragment se voit attribuer un sommet, et donc la coordonnée de texture qui va avec. Si un pixel est situé pile sur un sommet, la coordonnée de texture de ce sommet est attribuée au pixel. Si ce n'est pas le cas, la coordonnée de texture finale est interpolée à partir des coordonnées des trois sommets du triangle rastérisé. L'interpolation en question a lieu dans l'étape de rastérisation, comme nous l'avons vu dans le chapitre précédent. Le fait qu'il y ait une interpolation fait que les coordonnées du pixel gagent à être des nombres flottants. On pourrait faire une interpolation avec des coordonnées de texture entières, mais les arrondis et autres imprécisions de calcul donneraient un résultat graphiquement pas terrible, et empêcheraient d'utiliser les techniques de filtrage de texture que nous verrons dans ce chapitre.
À partir de ces coordonnées de texture, la carte graphique calcule l'adresse du texel qui correspond, et se charge de le lire. Et toute la magie a lieu dans ce calcul d'adresse, qui part de coordonnées de texture flottante, pour arriver à une adresse mémoire. Le calcul de l'adresse du texel se fait en plusieurs étapes, que nous allons voir ci-dessous. La première étape convertit les coordonnées flottantes en coordonnées entières, qui disent à quel ligne et colonne se trouve le texel voulu dans la texture. L'étape suivante transforme ces coordonnées x,y entières en adresse mémoire.
===La normalisation des coordonnées===
J'ai dit plus haut que les coordonnées de texture sont des coordonnées flottantes, comprises entre 0 et 1. Mais il faut savoir que les pixels shaders peuvent modifier celles-ci pour mettre en œuvre certains effets graphiques. Et le résultat peut alors se retrouver en-dehors de l'intervalle 0,1. C'est quelque chose de voulu et qui est traité par la carte graphique automatiquement, sans que ce soit une erreur. Au contraire, la manière dont la carte graphique traite cette situation permet d'implémenter des effets graphiques comme des textures en damier ou en miroir.
[[File:Clamp tile.jpg|vignette|Clamp tile]]
Il existe globalement trois méthodes très simples pour gérer cette situation, qui sont appelés des '''modes d'adressage de texture'''.
* La première méthode est de faire en sorte que le résultat sature. Si une coordonnée est inférieur à 0, alors on la remplace par un zéro. Si elle est supérieure à 1, on la ramène à 1. Avec cette méthode, tout se passe comme si les bords de la texture étaient étendus et remplissaient tout l'espace autour de la texture. Le tout est illustré ci-dessous. Ce mode d'accès aux textures est appelé le '''''clamp'''''.
* Une autre solution retire la partie entière de la coordonnée, elle coupe tout ce qui dépasse 1. Pour le dire autrement, elle calcule le résultat modulo 1 de la coordonnée. Le résultat est que tout se passe comme si la texture était répétée à l'infini et qu'elle pavait le plan.
* Une autre méthode remplit les coordonnées qui sortent de l’intervalle 0,1 avec une couleur préétablie, configurée par le programmeur.
===La conversion des coordonnées de textures flottantes en adresse mémoire===
Une fois la normalisation effectuée, les coordonnées de texture sont utilisées pour lire le texel voulu. Pour cela, les coordonnées de texte sont transformées en adresse mémoire, adresse qui pointe sur le texel ayant ces cordonnées. Pour cela, la première étape est de transformer les coordonnées flottantes u,v en coordonnées entières x,y qui pointent sur un texel. Pour cela, il suffit de multiplier les coordonnées flottantes u,v par la résolution de la texture accédée. Pour un écran de résolution <math>\text{height,width}</math>, le calcul est le suivant :
: <math>x = u \times \text{width}</math>
: <math>y = v \times \text{height}</math>
Le résultat est un nombre avec une partie entière et une partie fractionnaire. La partie entière des deux coordonnées donne la position x,y voulue, et la partie fractionnaire est conservée pour le filtrage de textures, mais passons cela sous silence pour le moment.
La seconde étape prend les coordonnées entières x,y et calcule l'adresse mémoire du texel. L'adresse dépend de la position de la texture en mémoire, précisément de son début, son premier texel, mais aussi de la position du texel par rapport au début de la texture. Et calculer cette position intra-texture dépend de la manière dont les texels sont stockés en mémoire.
====Les textures naïves====
Les programmeurs qui lisent ce cours s'attendent certainement à ce que la texture soit stockée en mémoire ligne par ligne, ou colonne par colonne. Cela veut dire que le premier pixel en partant d'en haut à gauche est stocké en premier, puis celui immédiatement à sa droite, puis celui encore à droite, et ainsi de suite. Une fois qu'on arrive à la fin d'une ligne, on passe à la ligne suivante, en-dessous. Cette organisation ligne par ligne s'appele l'organisation '''''row major order'''''. On peut faire pareil, mais colonne par colonne, ce qui donne le '''''column major order'''''.
[[File:Speicheranordnung Feld.svg|centre|vignette|upright=2|Row et column major order.]]
Maintenant, supposons que la texture commence à l'adresse <math>A_\text{texture}</math>, qui est l'adresse du premier texel. La texture a une résolution de <math>\text{width}</math> texels de large et <math>\text{height}</math> texels de haut. Par définition, les coordonnées X et Y des texels commencent à 0, ce qui fait que le pixel en haut à gauche a les coordonnées 0,0.
L'adresse du pixel se calcule comme suit :
: <math>A_\text{pixel} = A_\text{texture} + (\text{taille d'une ligne en octets} \times Y) + (\text{taille d'un texel en octets} \times X)</math>
La taille d'un pixel en mémoire est notée T. La taille d'une ligne en mémoire est de <math>width \times T</math>, par définition, vu qu'elle fait <math>width</math> texels. On a donc :
: <math>A_\text{pixel} = A_\text{texture} + (width \times T \times Y) + (T \times X)</math>
La formule se réécrit comme suit :
: <math>A_\text{pixel} = A_\text{texture} + T \times (width \times Y + X)</math>
Le calcul d'adresse est donc assez simple. Malheureusement, les textures ne sont pas stockées de cette manière en mémoire vidéo. En effet, elle se marie mal avec les opérations de filtrage de texture que nous allons voir dans ce qui suit. Le filtrage d'un texel dépend de ses voisins du dessus et du dessous. Le fait que la texture n'est pas forcément parcourue ligne par ligne fait que stocker une texture ligne par ligne n'est pas l'idéal.
De même, les textures sont déformées par la perspective. L'affichage de la texture ne se fait alors pas ligne par ligne, mais en parcourant la texture en diagonale, l'angle de la diagonale correspondant approximativement à l'angle que fait la verticale de la texture avec le regard. Vu qu'on ne connait pas à l'avance l'angle que fera la diagonale de parcours, on doit ruser.
====Les textures tilées====
Une première solution à ce problème est celle des '''textures tilées'''. Avec ces textures, l'image de la texture est découpée en ''tiles'', des rectangles ou en carrés de taille fixe, généralement des carrés de 4 pixels de côté. Les tiles ont une largeur et une longueur égales, afin de simplifier les calculs : on divise X et Y par le même nombre. De plus, leur largeur et leur longueur sont une puissance de deux, afin de simplifier les calculs d'adresse. Les ''tiles'' sont alors mémorisée les unes après les autres dans le fichier de la texture.
[[File:Texture tilée.png|centre|vignette|upright=2|Texture tilée]]
La formule de calcul d'adresse vue plus haut doit être adaptée pour tenir compte des tiles. Pour cela, il faut remplacer la taille d'un texel par la taille d'une tile, et que la largeur de la texture soit exprimée en nombre de tiles. De plus, on doit adapter les coordonnées des texels pour donner des coordonnées de tile. Généralement, les tiles sont des carrés de N pixels de côté, ce qui fait qu'on peut regrouper les lignes et les colonnes par paquets de N. Il suffit donc de diviser Y et X pour obtenir les coordonnées de la tile, de même que la larguer. La formule pour calculer la position de la énième tile est alors la suivante :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + \text{Taille mémoire d'une tile} \times \left( {\text{Width} \over N} \times {Y \over N} + {X \over N} \right)</math>
On peut réécrire le tout comme suit :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + K \times \left( {Y \over N} + X \right)</math>, avec K une constante connue à la compilation des shaders.
Vu que les tiles sont carrées avec une largeur qui est une puissance de deux, la multiplication par la taille d'une tile en mémoire se simplifie : on passe d'une multiplication entière à des décalages de bits. Même chose pour le calcul de l'adresse de la tile à partir des coordonnées x,y : ils impliquent des divisions par une puissance de deux, qui deviennent de simples décalages.
La position d'un pixel dans une tile dépend du format de la texture, mais peut se calculer avec quelques calculs arithmétiques simples. Dans les cas les plus simples, les pixels sont mémorisés ligne par ligne, ou colonne par colonne. Mais ce n'est pas systématiquement le cas. Toujours est-il que les calculs pour déterminer l'adresse sont simples, et ne demandent que quelques additions ou multiplications. Mais avec les formats de texture utilisés actuellement, les tiles sont chargées en entier dans le cache de texture, sans compter que diverses techniques de compression viennent mettre le bazar, comme on le verra dans la suite de cours.
Un avantage de l'organisation en tiles est qu'elle se marie bien avec le parcours des textures. On peut parcourir une texture dans tous les sens, horizontal, vertical, ou diagonal, on sait que les prochains pixels ont de fortes chances d'être dans la même tile. Si on rentre dans une tile par la gauche en haut, on a encore quelques pixels à parcourir dans la tile, par exemple. De même, le filtrage de textures est facilité. On verra dans ce qui va suivre que le filtrage de texture a besoin de lire des blocs de 4 texels, des carrés de 2 pixels de côté. Avec l'organisation en tile, on est certain que les 4 texels seront dans la même tile, sauf s'ils ont le malheur d'être tout au bord d'une tile. Ce dernier cas est assez rare, et il l'est d'autant plus que les tiles sont grandes. Enfin, un dernier avantage est que les tiles sont généralement assez petites pour tenir tout entier dans une ligne de cache. Le cache de texture est donc utilisé à merveille, ce qui rend les accès aux textures plus rapides.
====Les textures basées sur des ''z-order curves''====
Les formats de textures théoriquement optimaux utilisent une '''''Z-order curve''''', illustrée ci-dessous. L'idée est de découper la texture en quatre rectangles identiques, et de stocker ceux-ci les uns à la suite des autres. L'intérieur de ces rectangles est lui aussi découpé en quatre rectangles, et ainsi de suite. Au final, l'ordre des pixels en mémoire est celui illustré ci-dessous.
[[File:Z-CURVE.svg|centre|vignette|upright=2|Construction d'une ''Z-order curve''.]]
Les texels sont stockés les uns à la suite des autres dans la mémoire, en suivant l'ordre donnée par la ''Z-order curve''. Le calcul d'adresse calcule la position du texel en mémoire, par rapport au début de la texture, et ajoute l'adresse du début de la texture. Mais tout le défi est de calculer la position d'un texel en mémoire, à partir des coordonnées x,y. Le calcul peut sembler très compliqué, mais il n'en est rien. Le calcul demande juste de regarder les bits des deux coordonnées et de les combiner d'une manière particulièrement simple. Il suffit de placer le bit de poids fort de la coordonnée x, suivi de celui de la coordonnée y, et de faire ainsi de suite en passant aux bits suivants.
[[File:Zcurve45bits.png|centre|vignette|upright=1.5|Calcul de la position d'un élément dans une ''Z-order curve'' à partir des coordonnées x et y.]]
L'avantage d'une telle organisation est que la textures est découpées en ''tiles'' rectangulaires d'une certaine taille, elles-mêmes découpées en ''tiles'' plus petites, etc. Et il se trouve que cette organisation est parfaite pour le cache de texture. L'idéal pour le cache de texture est de charger une ''tile'' complète dans le cache de textures. Quand on accède à un texel, on s'assure que la ''tile'' complète soit chargée. Mais cela demande de connaitre à l'avance la taille d'une ''tile''. Les formats de texture fournissent généralement une ''tile'' carré de 4 pixels de côté, mais cela donnerait un cache trop petit pour être vraiment utile. Avec cette méthode, on s'assure qu'il y ait une ''tile'' avec la taille optimale. Les ''tiles'' étant découpées en ''tiles'' plus petites, elles-mêmes découpées, et ainsi de suite, on s'assure que la texture est découpées en ''tiles'' de taille variées. Il y aura au moins une ''tile'' qui rentrera tout pile dans le cache.
==Les techniques de rendu à textures multiples==
Nous venons de voir comment une texture est plaquée sur un objet 3D, ou une surface comme un sol. Pour résumer, le calcul de l'adresse d'un texel prend la position du texel par rapport au début de la texture, et ajoute l'adresse du début de la texture. L'adresse mémoire de la texture est connue au moment où le pilote de la carte graphique place la texture dans la mémoire vidéo, et cette information est transmise au matériel par l'intermédiaire du processeur de commande, puis passée aux processeurs de shaders et à l'unité de texture. Le tout est couplé à d'autres informations, la plus importante étant la ''taille de la texture en octets'', pour éviter de déborder lors des accès à la texture.
Néanmoins, il s'agit là du cas le plus simple. Certaines techniques de rendu demandent de choisir la texture à plaquer parmi un ensemble de plusieurs textures. Les techniques en question sont assez variées et n'ont pas grand chose en commun. Les plus connues sont le ''mip-mapping'', le ''cube-mapping'' et les textures virtuelles. Le ''mip-mapping'' sert à filtrer les textures, chose qu'on expliquera plus tard, le ''cube-mapping'' sert à simuler des réflexions sur un objet en plaquant une texture de l'environnement dessus, les textures virtuelles sont une optimisation pour les textures des terrains de grande taille. Mais malgré leurs différences, elles demandent de choisir quelle texture plaquer entre plusieurs textures de base. En clair, l'adresse de base de la texture varie selon la situation. Voyons-les dans le détail.
===Le mip-mapping===
Le '''mip-mapping''' a pour but de légèrement améliorer les graphismes des objets lointains, tout en rendant les calculs de texture plus rapides. Formellement, le ''mip-mapping'' est une technique de filtrage de texture, mais nous l'abordons maintenant car elle est surtout liée au calcul d'adresse. Les unités de texture ont des circuits de filtrage de texture séparés des circuits de ''mip-mapping'' et de calcul d'adresse, d'où le fait que nous en parlons séparément.
Le problème résolu par le ''mip-mapping'' est le rendu des textures lointaines. Si une texture est plaquée sur un objet lointain, une bonne partie des détails est invisible pour l'utilisateur. Un pixel de l'écran est associé à plusieurs texels. Idéalement, la carte graphiques devrait lire tous ces texels et en faire une sorte de moyenne pondérée, pour calculer la couleur finale du pixel. Mais dans les faits, ce serait très gourmand et compliqué à implémenter en hardware. Une solution serait de ne garder que quelque texels, mais cela a tendance à créer des artefacts visuels (les textures affichées ont tendance à pixeliser). Le ''mip-mapping'' permet de réduire ces deux problèmes en même temps en précalculant cette moyenne pondérée pour des distances prédéfinies.
L'idée est d'utiliser plusieurs exemplaires d'une même texture à des résolutions différentes, chaque exemplaire étant adapté à une certaine distance. Par exemple, une texture sera stocké avec un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre de 128 * 128 et ainsi de suite jusqu’à un dernier exemplaire de 32 * 32 pixel. Chaque exemplaire correspond à un '''niveau de détail''', aussi appelé ''Level Of Detail'' (abrévié en LOD). La résolution utilisée diminue d'autant plus que l'objet est situé loin de la caméra. Les objets proches seront rendus avec la texture 512*512, ceux plus lointains seront rendus avec la texture de résolution 256*256, les textures 128*128 seront utilisées encore plus loin, et ainsi de suite jusqu'aux objets les plus lointains qui sont rendus avec la texture la plus petite de 32*32.
[[File:MipMap Example STS101.jpg|centre|vignette|upright=2|Exemples de mip-maps.]]
Le ''mip-mapping'' améliore grandement la qualité d'image. L'image d'exemple ci-dessous le montre assez bien.
[[File:Mipmapping example.png|centre|vignette|upright=2|Exemple de mipmapping.]]
Pour faciliter les calculs d'adresse, les LOD d'une même texture sont stockées les uns après les autres en mémoire (dans un tableau, comme diraient les programmeurs). Ainsi, pas besoin de se souvenir de la position en mémoire de chaque LOD : l'adresse de la texture de base, et quelques astuces arithmétiques suffisent. Prenons le cas où la texture de base a une taille L. le premier exemplaire est à l'adresse 0, le second niveau de détail est à l'adresse L, le troisième à l'adresse L + L/4, le suivant à l'adresse L + L/4 + L/16, et ainsi de suite. Le calcul d'adresse demande juste connaître le niveau de détails souhaité et l'adresse de base de la texture. Le niveau de détail voulu est calculé par les pixel shaders, en fonction de la coordonnée de profondeur du pixel à traiter.
Évidemment, cette technique consomme de la mémoire vidéo, vu que chaque texture est dupliquée en plusieurs exemplaires, en plusieurs LOD. Dans le détail, la technique du mip-mapping prend au maximum 33% de mémoire en plus (sans compression). Cela vient du fait qu'en prenant une texture dexu fois plus petite, elle prend 4 fois moins de mémoire : 2 fois moins de pixels en largeur, et 2 fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des LODs, texture de base comprise, prendra X + (X/4) + (X/16) + (X/256) + … Un petit calcul de limite donne 4/3 * X, soit 33% de plus.
===Le cube-mapping===
[[File:Cube mapped reflection example 2.JPG|vignette|Exemple de reflets environnementaux.]]
L''''environnement-mapping''' est une technique de calcul de divers effets graphiques liés à l'environnement, notamment des réflexions. L'idée est de plaquer une texture pré-calculée pour simuler l'effet de l'environnement sur une surface ou un objet 3D. Il en existe plusieurs versions différentes, mais la seule utilisée de nos jours est le ''cube-mapping'', où la texture de l'environnement est plaquée sur un cube, d'où son nom. Le cube en question est utilisé différemment suivant ce que l'on cherche à faire avec le ''cube-mapping''. Les deux utilisations principales sont le rendu du ciel et des décors, et les réflexions sur la surface des objets. Dans les deux cas, l'idée est de précalculer ce que l'on voit du point de vue de la caméra. On place la caméra dans la scène 3D, on place un cube centré sur la caméra, le cube est texturé avec ce que l'on voit de l'environnement depuis la caméra/l'objet de son point de vue.
[[File:Panorama cube map.png|centre|vignette|upright=2|L'illustration montre en premier lieu une ''cubemap'' avec les six faces mises en évidence, puis quel environnement 3D elle permet de simuler, le troisième illustration montrant comment la ''cubemap'' est utilisée pour simuler l'environnement.]]
Le rendu du ciel et des décors lointains dans les jeux vidéo se base sur des '''''skybox''''', à savoir un cube centré sur la caméra, sur lequel on ajoute des textures de ciel ou de décors lointains. Le cube est recouvert par une texture, qui correspond à ce que l'on voit quand on dirige le regard de la caméra vers cette face. Contrairement à ce qu'on pourrait croire, la skybox n'est pas les limites de la scène 3D, les limites du niveau d'un jeu vidéo ou quoique ce soit d'autre de lié à la physique de la scène 3D. La skybox est centrée sur la caméra, elle suit la caméra dans son mouvement. Centrer la skybox sur la caméra permet de simuler des décors très lointains, suffisamment lointain pour qu'on n'ait pas l'illusion de s'en rapprocher en se déplaçant dans la map. De plus, cela évite d'avoir à faire trop de calculs à chaque fois que l'on bouge la caméra. La texture plaquée sur le cube est une texture unique, elle-même découpée en six sous-textures, une par face du cube.
[[File:Skybox example.png|centre|vignette|upright=2|Exemple de Skybox.]]
[[File:Cube mapped reflection example.jpg|vignette|Réflexions calculées par une ''cubemap''.]]
Le ''cube-mapping'' est aussi utilisé pour des reflets. L'idée est de simuler les reflets en plaquant une texture pré-calculée sur l'objet réflecteur. La texture pré-calculée est un dessin de l'environnement qui se reflète sur l'objet, un dessin du reflet à afficher. En la plaquant la texture sur l'objet, on simule ainsi des reflets de l'environnement, mais on ne peut pas calculer d'autres reflets comme les reflets objets mobiles comme les personnages. Et il se trouve que la texture pré-calculée est une ''cubemap''. Pour les environnements ouverts, c'est la ''skybox'' qui est utilisée, ce qui permet de simuler les reflets dans les flaques d'eau ou dans des lacs/océans/autres. Pour les environnements intérieurs, c'est une cubemap spécifique qui utilisée. Par exemple, pour l'intérieur d'une maison, on a une ''cubemap'' par pièce de la maison. Les reflets se calculent en précisant quelle ''cubemap'' appliquer sur l'objet en fonction de la direction du regard.
[[File:Cube map level.png|centre|vignette|Cube map de l'intérieur d'une pièce d'un niveau de jeux vidéo.]]
Toujours est-il que les textures utilisées pour le ''cubemmapping'', appelées des ''cubemaps'', sont en réalité la concaténation de six textures différentes. En mémoire vidéo, la ''cubemap'' est stockée comme six textures les unes à la suite des autres. Lors du rendu, on doit préciser quelle face du cube utiliser, ce qui fait 6 possibilités. On a le même problème qu'avec les niveaux de détail, sauf que ce sont les faces d'une ''cubemap'' qui remplacent les textures de niveaux de détails. L'accès en mémoire doit donc préciser quelle portion de la ''cubemap'' il faut accéder. Et l'accès mémoire se complexifie donc. Surtout que l'accès en question varie beaucoup suivant l'API graphique utilisée, et donc suivant la carte graphique.
Les API 3D assez anciennes ne gérent pas nativement les ''cubemaps'', qui doivent être émulées en logiciel en utilisant six textures différentes. Le pixel shader décide donc quelle ''cubemap'' utiliser, avec quelques calculs sur la direction du regard. L'accès se fait d'une manière assez simple : le shader choisit quelle texture utiliser. Les API 3D récentes gèrent nativement les ''cubemaps''. Dans le cas le plus simple,pour les versions les plus vielles de ces API, les six faces sont numérotées et l'accès à une ''cubemap'' précise quel face utiliser en donnant son numéro. La carte graphique choisit alors automatiquement la bonne texture, mais cela demande de laisser le calcul de la bonne face au pixel shader. D'autres API 3D et cartes graphiques font autrement. Dans les API 3D modenres, les ''cubemap'' sont gérées comme des textures en trois dimensions, adressées avec trois coordonnées u,v,w. La carte graphique utilise ces trois coordonnées de manière à en déduire quelle est la face pertinente, mais aussi les coordonnées u,v dans la texture de la face.
==L'implémentation matérielle du placage de textures==
Pour résumer, la lecture d'un texel demande d'effectuer plusieurs étapes. Dans le cas le plus simple, sans ''mip-mapping'' ou ''cubemapping'', on doit effectuer les étapes suivantes :
* Il faut d'abord normaliser les coordonnées de texture pour qu'elles tombent dans l'intervalle [0,1] en fonction du mode d'adressage désiré.
* Ensuite, les coordonnées u,v doivent être converties en coordonnées entières, ce qui demande une multiplication flottante.
* Enfin, l'adresse finale est calculée à partir des coordonnées entières et en ajoutant l'adresse de base de la texture (et éventuellement avec d'autres calculs arithmétiques suivant le format de la texture).
Tout cela pourrait être fait par le pixel shaders, mais cela implique beaucoup de calculs répétitifs et d'opérations arithmétiques assez lourdes, avec des multiplications flottantes, des additions et des multiplications entières, etc. Faire faire tous ces calculs par les shaders serait couteux en performance, sans compter que les shaders deviendraient plus gros et que cela aurait des conséquences sur le cache d'instruction. De plus, certaines de ces étapes peuvent se faire en parallèle, comme les deux premières, ce qui colle mal avec l'aspect sériel des shaders.
Aussi, les processeurs de shaders incorporent une unité de calcul d'adresse spéciale pour faire ces calculs directement en matériel. L'unité de texture contient au minimum deux circuits : un circuit de calcul d'adresse, et un circuit d'accès à la mémoire. Toute la difficulté tient dans le calcul d'adresse, plus que dans le circuit de lecture. Le calcul d'adresse est conceptuellement réalisé en deux étapes. La première étape qui transforme les coordonnées u,v en coordonnées x,y qui donne le numéro de la ligne et de la colonne du texel dans la texture. La seconde étape prend ces deux coordonnées x,y, l'adresse de la texture, et détermine l'adresse de la tile à lire.
[[File:Unité de texture simple.png|centre|vignette|upright=2|Unité de texture simple]]
===L'implémentation du mip-mapping===
Le ''mip-mapping'' est lui aussi pris en charge par l'unité de calcul d'adresse, car cette technique change l'adresse de base de la texture. La gestion du ''mip-mapping'' est cependant assez complexe. Il est possible de laisser le pixel shader calculer quel niveau de détail utiliser, en fonction de la coordonnée de profondeur z du pixel à afficher. La carte graphique détermine alors automatiquement quelle texture lire, quel niveau de détail, automatiquement. Elle détermine aussi la bonne résolution pour la texture, qui est égal à la résolution de la texture de base, divisée par le niveau de détail. Pour résumer, le niveau de détail est envoyé aux unités de texture, qui s'occupent de calculer l'adresse de base et la résolution adéquates. Quelques calculs arithmétiques simples, donc, qui s'implémentent facilement avec quelques circuits.
Mais une autre méthode laisse la carte graphique déterminer le niveau de détail par elle-même. Dans ce cas, cela demande, outre les deux coordonnées de texture, de calculer la dérivée de ces deux coordonnées dans le sens horizontal et vertical, ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Les quatre dérivées sont les suivantes :
: <math>\frac{du}{dx}</math>, <math>\frac{dv}{dx}</math>, <math>\frac{du}{dy}</math>, <math>\frac{dv}{dy}</math>
Un bon moyen pour obtenir les dérivées demande de regrouper les pixels par groupes de 4 et de faire la différence entre leurs coordonnées de texture respectives. On peut calculer les deux dérivées horizontales en comparant les deux pixels sur la même ligne, et les deux dérivées verticales en comparant les deux pixels sur la même colonne. Mais cela demande de rastériser les pixels par groupes de 4, par ''quads''. Et c'est ce qui est fait sur les cartes graphiques actuelles, qui rastérisent des groupes de 4 pixels à la fois.
[[File:Texture sampler unit with mipmapping.png|centre|vignette|upright=2.0|Unité de texture avec mipmapping.]]
Malheureusement, le calcul exact utilisé pour le choix de la mip-map dépend du GPU considéré et peu de chose est connu quant à ces algorithmes. Il est possible d'inférer le comportement à partir d'observations, mais guère plus. Pour ceux qui veulent en savoir plus, je conseille la lecture de cet article de blog :
* [https://pema.dev/2025/05/09/mipmaps-too-much-detail/ Mipmap selection in too much detail]
===La gestion des accès mémoire===
Enfin, l'unité de texture doit tenir compte du fait que la mémoire vidéo met du temps à lire une texture. En théorie, l'unité de texture ne devrait pas accepter de nouvelle demande de lecture tant que celle en cours n'est pas terminée. Mais faire ainsi demanderait de bloquer tout le pipeline, de l'''input assembler'' au unités de''shaders'', ce qui est tout sauf pratique et nuirait grandement aux performances.
Une solution alternative consiste à mettre en attente les demandes de lectures de texture pendant que la mémoire est occupée. La manière la plus simple d'implémenter des accès mémoire multiples est de les mettre en attente dans une petite mémoire FIFO. Cela implique que les accès mémoire s’exécutent dans l'ordre demandé par le ''shader'' et/ou l'unité de rastérisation, il n'y a pas de réorganisation des accès mémoire ou d’exécution dans le désordre des accès mémoire.
[[File:Texture prefetching.png|centre|vignette|upright=1.5|Accès mémoire simultanés.]]
Évidemment, quand la mémoire FIFO est pleine, le pipeline est alors totalement bloqué. Le rasteriser est prévenu que l'unité de texture ne peut pas accepter de nouvelle lecture de texture. En pratique, la FIFO est généralement d'une taille respectable et permet de mettre en attente beaucoup de demandes de lecture de texture. Il faut de plus noter qu'il y a une FIFO par processeur de ''shader'' sur les cartes graphiques modernes. Quand elle est pleine, le processeur cesse d'exécuter de nouveaux accès mémoire, mais peut continuer à exécuter des ''shaders'' dans les autres unités de calcul, pas besoin de bloquer complétement le pipeline.
===L'intégration du cache de textures===
Il faut noter que les unités de texture incorporent aussi un cache de texture, voire plusieurs. L'intégration des caches de texture avec la mémoire FIFO précédente est quelque peu compliqué, car il faut garantir que les lectures de texture se fassent dans le bon ordre. On ne peut pas exécuter une lecture dans le cache alors que des lectures précédentes sont en attente de lecture en mémoire vidéo. Et cela pose un gros problème : une lecture dans le cache de texture prend quelques dizaines de cycles d'horloge, alors qu'une lecture en mémoire vidéo en prend facilement 400 à 800 cycles, parfois plus. Et cela fait que l'ordre des accès mémoire peut s'inverser.
Prenons par exemple un accès au cache précédé et suivi par deux accès en mémoire vidéo. Le premier démarre au cycle 1, et se termine au cycle numéro 400. L'accès au cache commence au cycle 2 et se termine 20 cycles après, au cycle numéro 22. En clair, la lecture dans le cache s'est terminée avant l'accès mémoire qui le précède. Les textures ne sont donc plus lues dans l'ordre. Et il faut trouver une solution pour éviter cela.
La solution est de retarder les lectures dans le cache tant que tous les accès précédents ne sont pas terminés. Mais pour retarder les lectures en question, il faut d'abord savoir si la lecture atterrit dans le cache ou non, ce qui demande d'accéder au cache. On fait face à un dilemme : on veut retarder les accès au cache, mais les différencier des lectures déclenchant des accès mémoire demande d'accéder au cache en premier lieu. La solution est décrite dans l'article "Prefetching in a Texture Cache Architecture" par Igehy et ses collègues. Elle se base sur deux idées combinées ensemble.
La première idée est de séparer l'accès au cache en deux : une étape qui vérifie si les texels à lire sont dans le cache, et une étape qui accède aux données dans le cache lui-même. Un cache de texture est donc composé de deux circuits principaux. Le premier vérifie la présence des texels dans le cache. Il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Ensuite, en plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
La seconde idée est de retarder l'accès au cache entre les deux phases. La première étape d'un accès mémoire vérifie si la donnée est dans le cache ou non. Puis, on retarde la lecture des données, pour attendre que toutes les lectures précédentes soient terminées. Et enfin, troisième étape : la lecture des texels dans la mémoire cache proprement dite. Les accès mémoire passant par la mémoire vidéo se font de la même manière, à une différence près : la lecture dans le cache est remplacée par la lecture en mémoire vidéo. Tout démarre avec une demande à l'unité de tags, qui vérifie si le texel est dans le cache ou non. Puis on retarde l'accès tant que la mémoire vidéo est occupée, puis on effectue la lecture en mémoire vidéo.
Si ce n'est pas le cas, l'accès mémoire est envoyé à la mémoire vidéo comme précédemment, à savoir qu'il est mis en attente dans une mémoire FIFO, puis envoyé à la mémoire vidéo dès que celle-ci est libre. Mais en sortie de la mémoire, la donnée lue est envoyée dans le cache de texture, par dans l'unité de filtrage. Pour savoir où placer la donnée lue, l'unité de tag a réservé une ligne de cache précise, une adresse bien précise. L'adresse en question est disponible en lisant une autre mémoire FIFO, qui a mis en attente l'adresse en question, en attendant que l'accès mémoire se termine. La donnée est alors écrite dans le cache, puis lue par l'unité de filtrage de textures.
Pour une lecture dans le cache, le déroulement est similaire, mais sans le passage par la mémoire. La lecture fait une demande à l'unité de tag, et celle-ci répond que la donnée est bien dans le cache. Elle place alors l'adresse à lire dans la file d'attente. Une fois que les accès mémoire précédents sont terminés, l'adresse sort de la file d'attente et est envoyée à la mémoire de données. La lecture s'effectue, les texels sont envoyés à l'unité de filtrage de textures. La seule différence avec un ''phased cache'' normal est l'insertion de l'adresse à lire dans une FIFO qui vise à mettre en attente
[[File:Unité de texture avec un cache de texture.png|centre|vignette|upright=2.0|Unité de texture avec un cache de texture]]
Pour résumer, l'implémentation précédente garantit une exécution des lectures dans leur ordre d'arrivée. Et pour cela, elle retarde les lectures dans le cache tant que les lectures en mémoire précédentes ne sont pas terminées. L'accès au cache est plus rapide que l'accès en mémoire vidéo, mais le retard ajouté pour garantir l'ordre des lectures fait que le temps d'accès est très long.
==Le filtrage de textures==
Plaquer des textures sans autre forme de procès ne suffit pas à garantir des graphismes d'une qualité époustouflante. La raison est que les sommets et les texels ne tombent pas tout pile sur un pixel de l'écran : le sommet associé au texel peut être un petit peu trop en haut, ou trop à gauche, etc. Une explication plus concrète fait intervenir les coordonnées de texture. Souvenez-vous que lorsque l'on traduit une coordonnée de texture u,v en coordonnées x,y, on obtient un résultat qui ne tombe pas forcément juste. Souvent, le résultat a une partie fractionnaire. Si celle-ci est non-nulle, cela signifie que le texel/sommet n'est pas situé exactement sur le pixel voulu et que celui-ci est situé à une certaine distance. Concrètement, le pixel tombe entre quatre texels, comme indiqué ci-dessous.
[[File:Filtrage texture.png|centre|vignette|upright=2.0|Position du pixel par rapport aux texels.]]
Pour résoudre ce problème, on doit utiliser différentes techniques d'interpolation, aussi appelées techniques de '''filtrage de texture''', qui visent à calculer la couleur du pixel final en fonction des texels qui l'entourent. Il existe de nombreux types de filtrage de textures, qu'il s'agisse du filtrage linéaire, bilinéaire, trilinéaire, anisotropique et bien d'autres.
Tous ont besoin d'avoir certaines informations qui sont généralement fournies par les circuits de calcul d'adresse. La première est clairement la partie fractionnaire des coordonnées x,y. La seconde est la dérivée de ces deux coordonnées dans le sens horizontal et vertical., ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Toujours est-il que le filtrage de texture est une opération assez lourde, qui demande beaucoup de calculs arithmétiques. On pourrait en théorie le faire dans les pixels shaders, mais le cout en performance serait absolument insoutenable. Aussi, les cartes graphiques intègrent toutes un circuit dédié au filtrage de texture, le ''texture sampler''. Même les plus anciennes cartes graphiques incorporent une unité de filtrage de texture, ce qui nous montre à quel point cette opération est importante.
[[File:Texture unit.png|centre|vignette|upright=2.0|Unité de texture.]]
On peut configurer la carte graphique de manière à ce qu'elle fasse soit du filtrage bilinéaire, soit du filtrage trilinéaire, on peut configurer le niveau de filtrage anisotropique, etc. Cela peut se faire dans les options de la carte graphique, mais cela peut aussi être géré par l'application. La majorité des jeux vidéos permettent de régler cela dans les options. Ces réglages ne concernent pas la texture elle-même, mais plutôt la manière dont l'unité de texture doit fonctionner. Ces réglages sur l''''état de l'unité de texture''' sont mémorisés quelque part, soit dans l'unité de texture elle-même, soit fournies avec la ressource de texture elle-même, tout dépend de la carte graphique. Certaines cartes graphiques mémorisent ces réglages dans les unités de texture ou dans le processeur de commande, et tout changement demande alors de réinitialiser l'état des unités de texture, ce qui prend un peu de temps. D'autres placent ces réglages dans les ressources de texture elles-mêmes, ce qui rend les modifications de configuration plus rapides, mais demande plus de circuits. D'autres cartes graphiques mélangent les deux options, certains réglages étant globaux, d'autres transmis avec la texture. Bref, difficile de faire des généralités, tout dépend du matériel et le pilote de la carte graphique cache tout cela sous le tapis.
Maintenant que cela est dit, voyons quelles sont les différentes méthodes de filtrage de texture et comment la carte graphique fait pour les calculer.
===Le filtrage au plus proche===
La méthode de filtrage la plus simple consiste à colorier avec le texel le plus proche. Cela revient tout simplement à ne pas tenir compte de la partie fractionnaire des coordonnées x,y, ce qui est très simple à implémenter en matériel. C'est ce que l'on appelle le '''filtrage au plus proche''', aussi appelé ''nearest filtering''.
Autant être franc, le résultat est assez pixelisé et peu agréable à l’œil. Par contre, le résultat est très rapide à calculer, vu qu'il ne demande aucun calcul à proprement parler. Elle ne fait pas appel à la parti fractionnaire des coordonnées entières de texture, ni aux dérivées de ces coordonnées. On peut combiner cette technique avec le mip-mapping, ce qui donne un résultat bien meilleur, bien que loin d'être satisfaisant. Au passage, toutes les techniques de filtrage de texture peuvent se combiner avec du mip-mapping, certaines ne pouvant pas faire sans.
[[File:Interpolation-nearest.svg|centre|vignette|Filtrage de texture au plus proche.]]
===Le filtrage linéaire===
Le filtrage le plus simple est le '''filtrage linéaire'''. Il effectue une interpolation linéaire entre deux mip-maps, deux niveaux de détails. Pour comprendre l'idée, nous allons prendre une situation très simple, avec une texture carrée de 512 texels de côté. Le mip-mapping crée plusieurs textures : une de 256 texels de côté, une de 128 texels, une de 64, etc. Maintenant, la texture est sur un objet à une certaine distance de l'écran, vu de face. Le résultat est qu'elle correspond à l'écran à un carré de 300 pixels de côté (pas d'erreur : pixels, pas texels). Dans ce cas, la texture se trouve entre deux mip-maps : celle de 512 pixels de côté, celle de 256. Laquelle choisir ? Le filtrage au plus proche prend la texture de 512 pixels de côté. Le filtrage linéaire lui, fait autrement.
Vu que la texture est entre deux mip-maps, l'idée est de prendre le texel au plus proche dans chaque texture et de faire une sorte de moyenne appelée l'interpolation linéaire. L'interpolation par du principe que la couleur varie entre les deux texels en suivant une fonction affine, illustrée ci-dessous. Ce ne serait évidemment pas le cas dans le monde réel, mais on supposer cela donne une bonne approximation de ce à quoi ressemblerait une texture à plus haute résolution. On peut alors calculer la couleur du pixel par une simple moyenne pondérée par la distance. Le résultat est que les transitions entre deux niveaux de détails sont plus lisses, moins abruptes.
[[File:Lin interp -é.png|centre|vignette|upright=2.0|Interpolation linéaire.]]
===Le filtrage bilinéaire===
Le filtrage bilinéaire effectue une sorte de moyenne pondérée des quatre texels les plus proches du pixel à afficher. Pour cela, rappelez-vous ce qui a été dit plus haut : les coordonnées x,y d'un pixel ont une partie entière et une partie fractionnaire. Le filtrage au plus proche élimine les parties fractionnaires, ce qui donne une coordonnée x,y. Avec le filtrage bilinéaire, on prend les texels de coordonnées (x,y) ; (x+1,y) ; (x,y+1) ; (x+1,y+1), le pixel étant entre ces 4 texels.
Mais le filtrage ne fait pas qu'une simple moyenne, il prend en compte les parties fractionnaires pour faire la moyenne. En effet, le pixel n'est pas au milieu du carré de texel, il est quelque part mais est souvent plus proche d'un texel que des autres. Et il faut donc pondérer la moyenne par les distances aux 4 texels. Pour cela, la moyenne est calculée à partir d'interpolations linéaires. Avec 4 pixels, nous allons devoir calculer la couleur de deux points intermédiaires. La couleur de ces deux points se calcule par interpolation linéaire, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.
[[File:Bilin3.png|centre|vignette|upright=2|Filtrage bilinéaire de texture.]]
Le circuit qui permet de faire l'interpolation bilinéaire est particulièrement simple. On trouve un circuit de chaque pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu, et un pour la transparence. Chacun de ces circuit est composé de sous-circuits chargés d'effectuer une interpolation linéaire, reliés comme suit.
[[File:Texture sampler unit.png|centre|vignette|Unité de filtrage bilinéaire.]]
Vous noterez que le filtrage bilinéaire accède à 4 pixels en même temps. Fort heureusement, les textures sont stockées de manière à ce qu'on puisse charger les 4 pixels en une fois, comme on l'a vu plus haut. Le filtrage bilinéaire a de fortes chances que les 4 pixels filtrés soient dans la même ''tile'', la seule exception étant quand ils sont tout juste sur le bord d'une ''tile''.
: La console de jeu Nintendo 64 n'utilise que trois pixels au lieu de quatre dans son interpolation bilinéaire, qui en devient une interpolation quasi-bilinéaire. La raison derrière ce choix est une question de performances, comme beaucoup de décisions de ce genre. Le résultat est un rendu imparfait de certaines textures.
===Le filtrage trilinéaire===
Avec le filtrage bilinéaire, des discontinuités apparaissent sur certaines surfaces. Par exemple, pensez à une texture de sol : elle est appliquée plusieurs fois sur toute la surface du sol. A une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512*512 à 256*256, ce qui est visible pour un joueur attentif. De telles transitions sont lissées grâce au filtrage linéaire, il n'y a plus qu'à le combiner avec le filtrage bilinéaire. Rien d’incompatible : le premier filtre l'intérieur d'une mip-map, le second combine deux mip-maps.
Le filtrage trilinéaire prend les deux mip-maps les plus proches, fait un filtrage bilinéaire avec chacune, puis fait une « une moyenne » pondérée entre les deux résultats. Le circuit de filtrage trilinéaire existe en plusieurs versions. La plus simple, illustrée ci-dessous, effectue deux filtrages bilinéaires en parallèle, dans deux circuits séparés, puis combine leurs résultats avec un circuit d'interpolation linéaire. Mais ce circuit nécessite de charger 8 texels simultanément. Qui plus est, ces 8 texels ne sont pas consécutifs en mémoire, car ils sont dans deux niveaux de détails/mip-maps différents.
[[File:Parallel trilinear filtering.png|centre|vignette|upright=2.0|Unité de filtrage trilinéaire parallèle.]]
Vu qu'on lit des texels dans deux mip-maps, les texels sont lus en deux fois : 4 texels provenant de la première mip-map, suivis par les 4 texels de l'autre mip-map. Les 4 premiers texels doivent donc être mis en attente dans des registres, en attendant que les 4 autres arrivent. Une amélioration du circuit précédent gère cela en ajoutant des registres. Il lit les 4 premiers texels, les filtre avec une interpolation bilinéaire, et mémorise le résultat dans un registre. Puis, il lit les 4 autres texels, les filtre, et met le résultat dans un second registre. A ce moment là, un circuit d'interpolation linéaire finit le travail. On économise donc un circuit d'interpolation bilinéaire, sans que les performances soient trop impactées.
[[File:Filtrage trilineaire.png|centre|vignette|upright=1.0|Unité de filtrage trilineaire série.]]
Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le temps où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. À chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients, et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.
===Le filtrage anisotrope===
D'autres artefacts peuvent survenir lors de l'application d'une texture, la perspective pouvant déformer les textures et entraîner l'apparition de flou. La raison à cela est que les techniques de filtrage de texture précédentes partent du principe que la texture est vue de face. Prenez une texture carrée, par exemple. Vue de face, elle ressemble à un carré sur l'écran. Mais tournez la caméra, de manière à voir la texture de biais, avec un angle, et vous verrez que la forme de la texture sur l'écran est un trapèze, pas un carré. Cette déformation liée à la perspective n'est pas prise en compte par les méthodes de filtrage de texture précédentes. Pour le dire autrement, les techniques de filtrage précédentes partent du principe que les 4 texels qui entourent un pixel forment un carré, ce qui est vrai si la texture est vue de face, sans angle, mais ne l'est pas si la texture n'est pas perpendiculaire à l'axe de la caméra. Du point de vue de la caméra, les 4 texels forment un trapèze d'autant moins proche d'un carré que l'angle est grand.
Pour corriger cela, les chercheurs ont inventé le '''filtrage anisotrope'''. En fait, je devrais plutôt dire : LES filtrages anisotropes. Il en existe un grand nombre, dont certains ne sont pas utilisés dans les cartes graphiques actuelles, soit car ils trop gourmand en accès mémoires et en calculs pour être efficaces, soit car ils ne sont pas pratiques à mettre en œuvre. Il est très difficile de savoir quelles sont les techniques de filtrage de texture utilisées par les cartes graphiques, qu'elles soient récentes ou anciennes. Beaucoup de ces technologies sont brevetées ou gardées secrètes, et il faudrait vraiment creuser les brevets déposés par les fabricants de GPU pour en savoir plus. Les algorithmes en question seraient de plus difficiles à comprendre, les méthodes mathématiques cachées derrière ces méthodes de filtrage n'étant pas des plus simple.
[[File:Anisotropic filtering en.png|centre|vignette|upright=2|Exemple de filtrage anisotrope.]]
==La compression de textures==
Les textures les plus grosses peuvent aller jusqu'au mébioctet, ce qui est beaucoup. Pour limiter la casse, les textures sont compressées. La '''compression de texture''' réduit la taille des textures, ce qui peut se faire avec ou sans perte de qualité. Elle entraîne souvent une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande. Mais il s'agit là d'une technique très simple, beaucoup plus simple que les techniques que nous allons voir dans cette section. Nous allons voir quelque algorithmes de compression de textures de complexité intermédiaire, mais n'allons pas voir l'état de l'art. Il existe des formats de texture plus récents que ceux qui nous allons aborder, comme l{{'}}''Ericsson Texture Compression'' ou l{{'}}''Adaptive Scalable Texture Compression'', plus complexes et plus efficaces.
Notons que les textures sont compressées dans les fichiers du jeu, mais aussi en mémoire vidéo. Les textures sont décompressées lors de la lecture. Pour cela, la carte graphique contient alors un circuit, capable de décompresser les textures lorsqu'on les lit en mémoire vidéo. Les cartes graphiques supportent un grand nombre de formats de textures, au niveau du circuit de décompression. Du fait que les textures sont décompressées à la volée, les techniques de compression utilisées sont assez particulières. La carte graphique ne peut pas décompresser une texture entière avant de pouvoir l'utiliser dans un ''pixel shader''. A la place, on doit pouvoir lire un morceau de texture, et le décompresser à la volée. On ne peut utiliser les méthodes de compression du JPEG, ou d'autres formats de compression d'image. Ces dernières ne permettent pas de décompresser une image morceau par morceau.
Pour permettre une décompression/compression à la volée, les textures sont des textures tilées, généralement découpées en tiles de 4 * 4 texels. Les ''tiles'' sont compressées indépendamment les unes des autres. Et surtout, avec ou sans compression, la position des tiles en mémoire ne change pas. On trouve toujours une tile tous les T octets, peu importe que la tile soit compressée ou non. Par contre, une tile compressée n'occupera pas T octets, mais moins, là où une tile compressée occupera la totalité des T octets. En clair, compresser une tile fait qu'il y a des vides entre deux tiles dans al mémoire vidéo, mais ne change rien à leur place en mémoire vidéo qui est prédéterminée, peu importe que la texture soit compressée ou non. L'intérêt de la compression de textures n'est pas de réduire la taille de la texture en mémoire vidéo, mais de réduire la quantité de données à lire/écrire en mémoire vidéo. Au lieu de lire T octets pour une tile non-compressée, on pourra en lire moins.
===La palette indicée et la technique de ''Vector quantization''===
La technique de compression des textures la plus simple est celle de la '''palette indicée''', que l'on a entraperçue dans le chapitre sur les cartes d'affichage. La technique de '''''vector quantization''''' peut être vue comme une amélioration de la palette, qui travaille non pas sur des texels, mais sur des ''tiles''. À l'intérieur de la carte graphique, on trouve une table qui stocke toutes les ''tiles'' possibles. Chaque ''tile'' se voit attribuer un numéro, et la texture sera composé d'une suite de ces numéros. Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l’embarqué utilisent ce genre de compression.
===Les algorithmes de ''Block Truncation coding''===
La première technique de compression élaborée est celle du '''''Block Truncation Coding''''', qui ne marche que pour les images en niveaux de gris. Le BTC ne mémorise que deux niveaux de gris par ''tile'', que nous appellerons couleur 1 et couleur 2, les deux niveaux de gris n'étant pas le même d'une ''tile'' à l'autre. Chaque pixel d'une ''tile'' est obligatoirement colorié avec un de ces niveaux de gris. Pour chaque pixel d'une ''tile'', on mémorise sa couleur avec un bit : 0 pour couleur 1, et 1 pour couleur 2. Chaque ''tile'' est donc codée par deux entiers, qui codent chacun un niveau de gris, et une suite de bits pour les pixels proprement dit. Le circuit de décompression est alors vraiment très simple, comme illustré ci-dessous.
[[File:Block Truncation coding.jpg|centre|vignette|upright=2.0|Block Truncation coding.]]
La technique du BTC peut être appliquée non pas du des niveaux de gris, mais pour chaque composante Rouge, Vert et Bleu. Dans ces conditions, chaque ''tile'' est séparée en trois sous-''tiles'' : un sous-bloc pour la composante verte, un autre pour le rouge, et un dernier pour le bleu. Cela prend donc trois fois plus de place en mémoire que le BTC pur, mais cela permet de gérer les images couleur.
===Le format de compression S3TC / DXTC===
L'algorithme de '''Color Cell Compression''', ou CCC, améliore le BTC pour qu'il gère des couleurs autre que des niveaux de gris. Ce CCC remplace les deux niveaux de gris par deux couleurs. Une ''tile'' est donc codée avec un entier 32 bits par couleur, et une suite de bits pour les pixels. Le circuit de décompression est identique à celui utilisé pour le BTC.
[[File:Color Cell Compression.jpg|centre|vignette|Color Cell Compression.]]
[[File:Dxt1-memory-layout.png|vignette|Dxt1 et ''color cell compression''.]]
Le format de compression de texture utilisé de base par Direct X, le DXTC, est une version amliorée de l'algorithme précédent. Il est décliné en plusieurs versions : DXTC1, DXTC2, etc. La première version du DXTC est une sorte d'amélioration du CCC : il ajoute une gestion minimale de transparence, et découpe la texture à compresser en ''tiles'' de 4 pixels de côté. La différence, c'est que la couleur finale d'un texel est un mélange des deux couleurs attribuée au bloc. Pour indiquer comment faire ce mélange, on trouve deux bits de contrôle par texel.
Si jamais la couleur 1 < couleur2, ces deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (2 * Couleur1 + Couleur2) / 3
* 11 = (Couleur1 + 2 * Couleur2) / 3
Sinon, les deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (Couleur1 + Couleur2) / 2
* 11 = Transparent
[[File:DXTC.jpg|centre|vignette|DXTC.]]
Le circuit de décompression du DXTC ressemble alors à ceci :
[[File:Circuit de décompression du DXTC.jpg|centre|vignette|upright=2.0|Circuit de décompression du DXTC.]]
===Les format DXTC 2, 3, 4 et 5 : l'ajout de la transparence===
Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3, lui-même replacé par le DXT4 et par le DXT5. Dans le DXT3, la transparence fait son apparition. Pour cela, on ajoute 64 bits par ''tile'' pour stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1.
[[File:Dxt23-memory-layout.png|centre|vignette|Dxt 2 et 3.]]
Dans le DXT4 et le DXT5, la méthode utilisée pour compresser les couleurs l'est aussi pour les valeurs de transparence. L'information de transparence est stockée par un en-tête contenant deux valeurs de transparence, le tout suivi d'une matrice qui attribue trois bits à chaque texel. En fonction de la valeur des trois bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1.
[[File:Dxt45-memory-layout.png|centre|vignette|Dxt 4 et 5.]]
===Le format de compression PVRTC===
Passons maintenant à un format de compression de texture un peu moins connu, mais pourtant omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle travaille surtout dans les cartes graphiques embarquées. Ses cartes se trouvent notamment dans l'ipad, l'iPhone, et bien d'autres smartphones actuels.
Avec le PVRTC, les textures sont encore une fois découpées en ''tiles'' de 4 texels par 4, mais la ressemblance avec le DXTC s’arrête là. Chacque ''tile'' est codée avec :
* une couleur codée sur 16 bits ;
* une couleur codée sur 15 bits ;
* 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
* et un bit de modulation, qui permet de configurer l’interprétation des bits de mélange.
Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4*4.
==Annexe : les ''shadowmap'' hardware==
Les anciens GPU, notamment la Geforce FX, avaient des fonctionnalités spécifiques pour le calcul des ombres. Dans la plupart des jeux vidéos de l'époque, et même de maintenant, les ombres sont calculées avec la technique des ''shadowmap''. L'idée est assez simple sur le principe : un pixel est dans l'ombre quand il est invisible depuis une source de lumière.
L'idée est que le rendu est réalisé en plusieurs passes, avec une passe par source de lumière et une passe finale pour calculer l'image finale. Nous allons expliquer la technique avec une seule source de lumière, et allons utiliser l'exemple de la scène ci-dessous.
[[File:7fin.png|centre|vignette|Scène 3D d'exemple.]]
===La technique du ''shadowmapping''===
[[File:2shadowmap.png|vignette|Résultat de la première passe : ''shadowmap''..]]
La première passe rend l'image depuis le point de vue de la source de lumière. Cette première passe ne rend pas les couleurs de la scène, elle ne s'intéresse qu'à la profondeur des pixels. Le résultat est que l'image ne rend que le tampon de profondeur. Celui-ci est ensuite réutilisé comme texture pour la passe suivante. La texture en question est appelée la '''''shadownmap'''''.
La perspective utilisée, ainsi que le ''view frustrum'', dépend de la source de lumière. Pour une source de lumière qui émet un cône de lumière, le ''view frustrum'' de l'image rendue doit contenir tout le cone de lumière, et doit coller le plus possible à celui-ci. Pour une source directionnelle, comme le soleil, une perspective orthographique est utilisée.
La seconde passe rend l'image du point de vue de la caméra, pour rendre l'image finale. Elle rend l'image finale, qui est composée de pixels, chacun ayant une position à l'écran x,y, et une profondeur z. Les coordonnées sont transformées pour obtenir la position de ce pixel depuis le point de vue de la caméra. Une simple multiplication de matrice suffit, rien de bien compliqué, un shader peut le faire.
[[File:5failed.png|vignette|Résultat du test des comparaisons.]]
Après cette étape, on a alors les coordonnées x,y,z de ce pixel du point de vue de la caméra, et la ''shadowmap''. Il est alors possible d'accéder à la ''shadowmap'' au même endroit, à la même place que le pixel testé, aux mêmes coordonnées x,y. Si la profondeur du pixel est supérieure à celle de la shadowmap au même endroit, alors le pixel est situé derrière la surface visible, donc est dans l'ombre. Sinon, il n'est pas dans l'ombre. Le même procédé est répété sur chaque pixel de l'écran.
===Les optimisations hardware du ''shadowmapping''===
La technique des ''shadowmap'' demande donc de calculer une texture ''shadowmap'', puis de lire celle-ci et de faire des comparaisons de profondeur. Les GPU comme la Geforce FX intégraient du matériel dans les unités de texture pour faciliter ce travail. Les unités de texture pouvaient lire les ''shadowmap'', et faire la comparaison de profondeur toutes seules, elles avaient des circuits pour. Il suffisait de leur fournir le pixel à tester, ses coordonnées x,y,z, et l'adresse de la ''shadowmap''. Les unités de texture renvoyaient alors un résultat valant 0 ou 1 : 1 si le pixel est dans l'ombre, 0 sinon.
Elles pouvaient même effectuer du filtrage de texture sur les ''shadowmap''. Mais le filtrage était différent de celui utilisé sur les autres textures : moyenner des valeurs de profondeur ne marche pas bien. Elles utilisaient des techniques de filtrage différentes : elles faisaient les tests de comparaison, puis faisaient la moyenne des résultats. Ainsi, pour du filtrage bilinéaire, elles lisaient 4 texels dans la ''shadowmap'', puis faisaient 4 tests de comparaison, et moyennaient les 4 résultats.
==Annexe : les textures virtuelles==
Les '''textures virtuelles''' sont une optimisation des textures normales, qui visent à accélérer le rendu de terrains de grande taille. Imaginez par exemple un monde assez ouvert, comme un environnement en forêt ou en montagne, avec une grande distance de visibilité. Avec de tels terrains, le "sol" est recouvert par une texture de sol unique qui recouvre tout le terrain. Elle ne se répète pas, est de très grande taille, et peut parfois recouvrir toute la map ! Mais il n'y a pas assez de mémoire vidéo pour mémoriser la texture toute entière. La seule solution est la suivante : une partie de la texture est placée en mémoire vidéo, le reste est soit placé en mémoire RAM ou sur le disque dur.
Pour cela, le moteur de jeu utilise une optimisation ingénieuse, basée sur une observation assez basique : une bonne partie de la texture est visible, mais le reste est caché par des arbres, des habitations ou d'autres obstacles. Une optimisation possible de ne garder en mémoire vidéo que les portions visibles de la texture, pas les portions cachées. Une autre optimisation mélange textures virtuelles et ''mip-mapping''. L'idée est que pour les portions lointaines d'une texture, la texture utilisée est une ''mip-map'' de basse résolution. L'idée est alors de ne charger que la ''mip-map'' adéquate, pas les autres niveaux de détail. En clair, la texture de base n'est pas chargée en mémoire vidéo, mais la ''mip-map'' basse résolution l'est.
===Une texture à deux niveaux===
L'implémentation des textures virtuelles découpe les méga-textures en ''tiles'', en morceaux rectangulaires de taille modeste. En clair, le terrain est découpé en morceau rectangulaires/carrés. Seules les tiles nécessaires sont chargées en mémoire vidéo, pas les autres. Par exemple, les ''tiles'' non-visibles ne sont pas placées en mémoire vidéo, seules les ''tiles'' visibles le sont. De même, il y a une ''tile'' par niveau de mip-map : seul la tile correspondant le niveau adéquat est en mémoire vidéo, les autres niveaux de détail ne sont pas chargés. On peut faire une analogie avec la mémoire virtuelle, où les données sont découpées en pages, qui sont chargées en mémoire RAM à la demande, suivant les besoins, les données pouvant être swappées sur le disque dur si elles sont peu utilisées. Sauf qu'ici, il s'agit de textures qui sont découpées en pages chargées à la demande en mémoire vidéo, depuis la RAM système.
Une texture virtuelle est en réalité un système à deux niveaux : une liste de ''tiles'' et les ''tiles'' elles-mêmes. La liste de ''tiles'' est appelée un '''atlas de texture''', c'est un peu l'équivalent de la ''tilemap'' pour le rendu 2D. Rendre une texture demande de calculer quelle ''tile'' contient le texel à afficher, consulter la ''tile'' en question, puis récupérer le texel adéquat dans cette ''tile''. La ''tile'' est donc une texture, mais la texture à charger est choisie parmi un ensemble, qui est ici l'atlas de texture.
===L'implémentation : logicielle versus matérielle===
Les textures virtuelles ont été utilisées pour la première fois par les jeux Rage 1 et 2 d'IdSoftware, et quelques jeux ultérieurs comme DOOM 2016. IdSoftware les appelait des '''''mega-textures'''''. L'optimisation permettait des gains en performance assez impressionnants. Le jeu Rage 1 utilisait une texture carrée unique de 128k pixels de côté pour rendre le terrain. En théorie, une telle texture devrait prendre 64 giga-octets, mais le jeu tournait correctement avec 512 méga-octets de RAM, poussivement avec seulement 256 méga-octets de RAM.
De nos jours, les textures virtuelles sont supportées par beaucoup de jeux vidéos, les moteurs les plus courants gèrent de telles textures de manière logicielles. Mais quelques GPU récents supportent les textures virtuelles. Sur les GPU récents, l'atlas de texture est géré nativement par le matériel. Le GPU choisit quelle ''tile'', quelle texture choisir pour rendre le texel adéquat. Pour cela, le GPU calcule quelle ''tile'' charger, consulte l'atlas de texture, et lit la texture de ''tile'' adéquate.
Mais l'implémentation sur les GPU récents a de nombreuses limitations. La limitation la plus importante est que la taille des textures virtuelles ne peut pas dépasser la taille d'une texture normale, soit 32768 pixels de côté pour une texture carrée environ sur les GPU de 2020. De plus, le chargement d'une ''tile'' est très lent. En clair, dès qu'on veut changer de niveau de mip-map pour une tile, ou dès qu'une tile devient visible, le chargement de la tile peut facilement prendre plusieurs centaines de millisecondes. Le filtrage de texture est très complexe avec des textures virtuelles, ce qui fait que le filtrage de texture virtuelle est souvent soumis à des limitations que les textures normales n'ont pas, notamment pour le filtrage anisotropique.
{{NavChapitre | book=Les cartes graphiques
| prev=Le rasterizeur
| prevText=Le rasterizeur
| next=Les Render Output Target
| nextText=Les Render Output Target
}}{{autocat}}
rqtdyguk6i981zsuhsctbiwlqlg3znk
763405
763400
2026-04-10T16:54:47Z
Mewtow
31375
/* Annexe : les shadowmap hardware */
763405
wikitext
text/x-wiki
[[File:Texture mapping.png|vignette|''Texture mapping'']]
Les '''textures''' sont des images que l'on va plaquer sur la surface d'un objet, du papier peint en quelque sorte. Les cartes graphiques supportent divers formats de textures, qui indiquent comment les pixels de l'image sont stockés en mémoire : RGB, RGBA, niveaux de gris, etc. Une texture est donc composée de "pixels", comme toute image numérique. Pour bien faire la différence entre les pixels d'une texture, et les pixels de l'écran, les pixels d'une texture sont couramment appelés des ''texels''.
==Le placage de textures inverse==
Pour rappel, plaquer une texture sur un objet consiste à attribuer un texel à chaque sommet, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture.
Dans les faits, on n'utilise pas de coordonnées entières de ce type. Les coordonnées de texture sont deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. Le nom donnée à cette technique de description des coordonnées de texture s'appelle l''''''UV Mapping'''''.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Les API 3D modernes gèrent des textures en trois dimensions, ce qui ajoute une troisième coordonnée de texture notée w. Dans ce qui va suivre, nous allons passer les textures en trois dimensions sous silence. Elles ne sont pas très utilisées, la quasi-totalité des jeux vidéo et applications 3D utilisant des textures en deux dimensions. Par contre, le matériel doit gérer les textures 3D, ce qui le rend plus complexe que prévu. Il faut ajouter quelques circuits pour, de quoi gérer la troisième coordonnée de texture, etc.
Lors de la rastérisation, chaque fragment se voit attribuer un sommet, et donc la coordonnée de texture qui va avec. Si un pixel est situé pile sur un sommet, la coordonnée de texture de ce sommet est attribuée au pixel. Si ce n'est pas le cas, la coordonnée de texture finale est interpolée à partir des coordonnées des trois sommets du triangle rastérisé. L'interpolation en question a lieu dans l'étape de rastérisation, comme nous l'avons vu dans le chapitre précédent. Le fait qu'il y ait une interpolation fait que les coordonnées du pixel gagent à être des nombres flottants. On pourrait faire une interpolation avec des coordonnées de texture entières, mais les arrondis et autres imprécisions de calcul donneraient un résultat graphiquement pas terrible, et empêcheraient d'utiliser les techniques de filtrage de texture que nous verrons dans ce chapitre.
À partir de ces coordonnées de texture, la carte graphique calcule l'adresse du texel qui correspond, et se charge de le lire. Et toute la magie a lieu dans ce calcul d'adresse, qui part de coordonnées de texture flottante, pour arriver à une adresse mémoire. Le calcul de l'adresse du texel se fait en plusieurs étapes, que nous allons voir ci-dessous. La première étape convertit les coordonnées flottantes en coordonnées entières, qui disent à quel ligne et colonne se trouve le texel voulu dans la texture. L'étape suivante transforme ces coordonnées x,y entières en adresse mémoire.
===La normalisation des coordonnées===
J'ai dit plus haut que les coordonnées de texture sont des coordonnées flottantes, comprises entre 0 et 1. Mais il faut savoir que les pixels shaders peuvent modifier celles-ci pour mettre en œuvre certains effets graphiques. Et le résultat peut alors se retrouver en-dehors de l'intervalle 0,1. C'est quelque chose de voulu et qui est traité par la carte graphique automatiquement, sans que ce soit une erreur. Au contraire, la manière dont la carte graphique traite cette situation permet d'implémenter des effets graphiques comme des textures en damier ou en miroir.
[[File:Clamp tile.jpg|vignette|Clamp tile]]
Il existe globalement trois méthodes très simples pour gérer cette situation, qui sont appelés des '''modes d'adressage de texture'''.
* La première méthode est de faire en sorte que le résultat sature. Si une coordonnée est inférieur à 0, alors on la remplace par un zéro. Si elle est supérieure à 1, on la ramène à 1. Avec cette méthode, tout se passe comme si les bords de la texture étaient étendus et remplissaient tout l'espace autour de la texture. Le tout est illustré ci-dessous. Ce mode d'accès aux textures est appelé le '''''clamp'''''.
* Une autre solution retire la partie entière de la coordonnée, elle coupe tout ce qui dépasse 1. Pour le dire autrement, elle calcule le résultat modulo 1 de la coordonnée. Le résultat est que tout se passe comme si la texture était répétée à l'infini et qu'elle pavait le plan.
* Une autre méthode remplit les coordonnées qui sortent de l’intervalle 0,1 avec une couleur préétablie, configurée par le programmeur.
===La conversion des coordonnées de textures flottantes en adresse mémoire===
Une fois la normalisation effectuée, les coordonnées de texture sont utilisées pour lire le texel voulu. Pour cela, les coordonnées de texte sont transformées en adresse mémoire, adresse qui pointe sur le texel ayant ces cordonnées. Pour cela, la première étape est de transformer les coordonnées flottantes u,v en coordonnées entières x,y qui pointent sur un texel. Pour cela, il suffit de multiplier les coordonnées flottantes u,v par la résolution de la texture accédée. Pour un écran de résolution <math>\text{height,width}</math>, le calcul est le suivant :
: <math>x = u \times \text{width}</math>
: <math>y = v \times \text{height}</math>
Le résultat est un nombre avec une partie entière et une partie fractionnaire. La partie entière des deux coordonnées donne la position x,y voulue, et la partie fractionnaire est conservée pour le filtrage de textures, mais passons cela sous silence pour le moment.
La seconde étape prend les coordonnées entières x,y et calcule l'adresse mémoire du texel. L'adresse dépend de la position de la texture en mémoire, précisément de son début, son premier texel, mais aussi de la position du texel par rapport au début de la texture. Et calculer cette position intra-texture dépend de la manière dont les texels sont stockés en mémoire.
====Les textures naïves====
Les programmeurs qui lisent ce cours s'attendent certainement à ce que la texture soit stockée en mémoire ligne par ligne, ou colonne par colonne. Cela veut dire que le premier pixel en partant d'en haut à gauche est stocké en premier, puis celui immédiatement à sa droite, puis celui encore à droite, et ainsi de suite. Une fois qu'on arrive à la fin d'une ligne, on passe à la ligne suivante, en-dessous. Cette organisation ligne par ligne s'appele l'organisation '''''row major order'''''. On peut faire pareil, mais colonne par colonne, ce qui donne le '''''column major order'''''.
[[File:Speicheranordnung Feld.svg|centre|vignette|upright=2|Row et column major order.]]
Maintenant, supposons que la texture commence à l'adresse <math>A_\text{texture}</math>, qui est l'adresse du premier texel. La texture a une résolution de <math>\text{width}</math> texels de large et <math>\text{height}</math> texels de haut. Par définition, les coordonnées X et Y des texels commencent à 0, ce qui fait que le pixel en haut à gauche a les coordonnées 0,0.
L'adresse du pixel se calcule comme suit :
: <math>A_\text{pixel} = A_\text{texture} + (\text{taille d'une ligne en octets} \times Y) + (\text{taille d'un texel en octets} \times X)</math>
La taille d'un pixel en mémoire est notée T. La taille d'une ligne en mémoire est de <math>width \times T</math>, par définition, vu qu'elle fait <math>width</math> texels. On a donc :
: <math>A_\text{pixel} = A_\text{texture} + (width \times T \times Y) + (T \times X)</math>
La formule se réécrit comme suit :
: <math>A_\text{pixel} = A_\text{texture} + T \times (width \times Y + X)</math>
Le calcul d'adresse est donc assez simple. Malheureusement, les textures ne sont pas stockées de cette manière en mémoire vidéo. En effet, elle se marie mal avec les opérations de filtrage de texture que nous allons voir dans ce qui suit. Le filtrage d'un texel dépend de ses voisins du dessus et du dessous. Le fait que la texture n'est pas forcément parcourue ligne par ligne fait que stocker une texture ligne par ligne n'est pas l'idéal.
De même, les textures sont déformées par la perspective. L'affichage de la texture ne se fait alors pas ligne par ligne, mais en parcourant la texture en diagonale, l'angle de la diagonale correspondant approximativement à l'angle que fait la verticale de la texture avec le regard. Vu qu'on ne connait pas à l'avance l'angle que fera la diagonale de parcours, on doit ruser.
====Les textures tilées====
Une première solution à ce problème est celle des '''textures tilées'''. Avec ces textures, l'image de la texture est découpée en ''tiles'', des rectangles ou en carrés de taille fixe, généralement des carrés de 4 pixels de côté. Les tiles ont une largeur et une longueur égales, afin de simplifier les calculs : on divise X et Y par le même nombre. De plus, leur largeur et leur longueur sont une puissance de deux, afin de simplifier les calculs d'adresse. Les ''tiles'' sont alors mémorisée les unes après les autres dans le fichier de la texture.
[[File:Texture tilée.png|centre|vignette|upright=2|Texture tilée]]
La formule de calcul d'adresse vue plus haut doit être adaptée pour tenir compte des tiles. Pour cela, il faut remplacer la taille d'un texel par la taille d'une tile, et que la largeur de la texture soit exprimée en nombre de tiles. De plus, on doit adapter les coordonnées des texels pour donner des coordonnées de tile. Généralement, les tiles sont des carrés de N pixels de côté, ce qui fait qu'on peut regrouper les lignes et les colonnes par paquets de N. Il suffit donc de diviser Y et X pour obtenir les coordonnées de la tile, de même que la larguer. La formule pour calculer la position de la énième tile est alors la suivante :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + \text{Taille mémoire d'une tile} \times \left( {\text{Width} \over N} \times {Y \over N} + {X \over N} \right)</math>
On peut réécrire le tout comme suit :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + K \times \left( {Y \over N} + X \right)</math>, avec K une constante connue à la compilation des shaders.
Vu que les tiles sont carrées avec une largeur qui est une puissance de deux, la multiplication par la taille d'une tile en mémoire se simplifie : on passe d'une multiplication entière à des décalages de bits. Même chose pour le calcul de l'adresse de la tile à partir des coordonnées x,y : ils impliquent des divisions par une puissance de deux, qui deviennent de simples décalages.
La position d'un pixel dans une tile dépend du format de la texture, mais peut se calculer avec quelques calculs arithmétiques simples. Dans les cas les plus simples, les pixels sont mémorisés ligne par ligne, ou colonne par colonne. Mais ce n'est pas systématiquement le cas. Toujours est-il que les calculs pour déterminer l'adresse sont simples, et ne demandent que quelques additions ou multiplications. Mais avec les formats de texture utilisés actuellement, les tiles sont chargées en entier dans le cache de texture, sans compter que diverses techniques de compression viennent mettre le bazar, comme on le verra dans la suite de cours.
Un avantage de l'organisation en tiles est qu'elle se marie bien avec le parcours des textures. On peut parcourir une texture dans tous les sens, horizontal, vertical, ou diagonal, on sait que les prochains pixels ont de fortes chances d'être dans la même tile. Si on rentre dans une tile par la gauche en haut, on a encore quelques pixels à parcourir dans la tile, par exemple. De même, le filtrage de textures est facilité. On verra dans ce qui va suivre que le filtrage de texture a besoin de lire des blocs de 4 texels, des carrés de 2 pixels de côté. Avec l'organisation en tile, on est certain que les 4 texels seront dans la même tile, sauf s'ils ont le malheur d'être tout au bord d'une tile. Ce dernier cas est assez rare, et il l'est d'autant plus que les tiles sont grandes. Enfin, un dernier avantage est que les tiles sont généralement assez petites pour tenir tout entier dans une ligne de cache. Le cache de texture est donc utilisé à merveille, ce qui rend les accès aux textures plus rapides.
====Les textures basées sur des ''z-order curves''====
Les formats de textures théoriquement optimaux utilisent une '''''Z-order curve''''', illustrée ci-dessous. L'idée est de découper la texture en quatre rectangles identiques, et de stocker ceux-ci les uns à la suite des autres. L'intérieur de ces rectangles est lui aussi découpé en quatre rectangles, et ainsi de suite. Au final, l'ordre des pixels en mémoire est celui illustré ci-dessous.
[[File:Z-CURVE.svg|centre|vignette|upright=2|Construction d'une ''Z-order curve''.]]
Les texels sont stockés les uns à la suite des autres dans la mémoire, en suivant l'ordre donnée par la ''Z-order curve''. Le calcul d'adresse calcule la position du texel en mémoire, par rapport au début de la texture, et ajoute l'adresse du début de la texture. Mais tout le défi est de calculer la position d'un texel en mémoire, à partir des coordonnées x,y. Le calcul peut sembler très compliqué, mais il n'en est rien. Le calcul demande juste de regarder les bits des deux coordonnées et de les combiner d'une manière particulièrement simple. Il suffit de placer le bit de poids fort de la coordonnée x, suivi de celui de la coordonnée y, et de faire ainsi de suite en passant aux bits suivants.
[[File:Zcurve45bits.png|centre|vignette|upright=1.5|Calcul de la position d'un élément dans une ''Z-order curve'' à partir des coordonnées x et y.]]
L'avantage d'une telle organisation est que la textures est découpées en ''tiles'' rectangulaires d'une certaine taille, elles-mêmes découpées en ''tiles'' plus petites, etc. Et il se trouve que cette organisation est parfaite pour le cache de texture. L'idéal pour le cache de texture est de charger une ''tile'' complète dans le cache de textures. Quand on accède à un texel, on s'assure que la ''tile'' complète soit chargée. Mais cela demande de connaitre à l'avance la taille d'une ''tile''. Les formats de texture fournissent généralement une ''tile'' carré de 4 pixels de côté, mais cela donnerait un cache trop petit pour être vraiment utile. Avec cette méthode, on s'assure qu'il y ait une ''tile'' avec la taille optimale. Les ''tiles'' étant découpées en ''tiles'' plus petites, elles-mêmes découpées, et ainsi de suite, on s'assure que la texture est découpées en ''tiles'' de taille variées. Il y aura au moins une ''tile'' qui rentrera tout pile dans le cache.
==Les techniques de rendu à textures multiples==
Nous venons de voir comment une texture est plaquée sur un objet 3D, ou une surface comme un sol. Pour résumer, le calcul de l'adresse d'un texel prend la position du texel par rapport au début de la texture, et ajoute l'adresse du début de la texture. L'adresse mémoire de la texture est connue au moment où le pilote de la carte graphique place la texture dans la mémoire vidéo, et cette information est transmise au matériel par l'intermédiaire du processeur de commande, puis passée aux processeurs de shaders et à l'unité de texture. Le tout est couplé à d'autres informations, la plus importante étant la ''taille de la texture en octets'', pour éviter de déborder lors des accès à la texture.
Néanmoins, il s'agit là du cas le plus simple. Certaines techniques de rendu demandent de choisir la texture à plaquer parmi un ensemble de plusieurs textures. Les techniques en question sont assez variées et n'ont pas grand chose en commun. Les plus connues sont le ''mip-mapping'', le ''cube-mapping'' et les textures virtuelles. Le ''mip-mapping'' sert à filtrer les textures, chose qu'on expliquera plus tard, le ''cube-mapping'' sert à simuler des réflexions sur un objet en plaquant une texture de l'environnement dessus, les textures virtuelles sont une optimisation pour les textures des terrains de grande taille. Mais malgré leurs différences, elles demandent de choisir quelle texture plaquer entre plusieurs textures de base. En clair, l'adresse de base de la texture varie selon la situation. Voyons-les dans le détail.
===Le mip-mapping===
Le '''mip-mapping''' a pour but de légèrement améliorer les graphismes des objets lointains, tout en rendant les calculs de texture plus rapides. Formellement, le ''mip-mapping'' est une technique de filtrage de texture, mais nous l'abordons maintenant car elle est surtout liée au calcul d'adresse. Les unités de texture ont des circuits de filtrage de texture séparés des circuits de ''mip-mapping'' et de calcul d'adresse, d'où le fait que nous en parlons séparément.
Le problème résolu par le ''mip-mapping'' est le rendu des textures lointaines. Si une texture est plaquée sur un objet lointain, une bonne partie des détails est invisible pour l'utilisateur. Un pixel de l'écran est associé à plusieurs texels. Idéalement, la carte graphiques devrait lire tous ces texels et en faire une sorte de moyenne pondérée, pour calculer la couleur finale du pixel. Mais dans les faits, ce serait très gourmand et compliqué à implémenter en hardware. Une solution serait de ne garder que quelque texels, mais cela a tendance à créer des artefacts visuels (les textures affichées ont tendance à pixeliser). Le ''mip-mapping'' permet de réduire ces deux problèmes en même temps en précalculant cette moyenne pondérée pour des distances prédéfinies.
L'idée est d'utiliser plusieurs exemplaires d'une même texture à des résolutions différentes, chaque exemplaire étant adapté à une certaine distance. Par exemple, une texture sera stocké avec un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre de 128 * 128 et ainsi de suite jusqu’à un dernier exemplaire de 32 * 32 pixel. Chaque exemplaire correspond à un '''niveau de détail''', aussi appelé ''Level Of Detail'' (abrévié en LOD). La résolution utilisée diminue d'autant plus que l'objet est situé loin de la caméra. Les objets proches seront rendus avec la texture 512*512, ceux plus lointains seront rendus avec la texture de résolution 256*256, les textures 128*128 seront utilisées encore plus loin, et ainsi de suite jusqu'aux objets les plus lointains qui sont rendus avec la texture la plus petite de 32*32.
[[File:MipMap Example STS101.jpg|centre|vignette|upright=2|Exemples de mip-maps.]]
Le ''mip-mapping'' améliore grandement la qualité d'image. L'image d'exemple ci-dessous le montre assez bien.
[[File:Mipmapping example.png|centre|vignette|upright=2|Exemple de mipmapping.]]
Pour faciliter les calculs d'adresse, les LOD d'une même texture sont stockées les uns après les autres en mémoire (dans un tableau, comme diraient les programmeurs). Ainsi, pas besoin de se souvenir de la position en mémoire de chaque LOD : l'adresse de la texture de base, et quelques astuces arithmétiques suffisent. Prenons le cas où la texture de base a une taille L. le premier exemplaire est à l'adresse 0, le second niveau de détail est à l'adresse L, le troisième à l'adresse L + L/4, le suivant à l'adresse L + L/4 + L/16, et ainsi de suite. Le calcul d'adresse demande juste connaître le niveau de détails souhaité et l'adresse de base de la texture. Le niveau de détail voulu est calculé par les pixel shaders, en fonction de la coordonnée de profondeur du pixel à traiter.
Évidemment, cette technique consomme de la mémoire vidéo, vu que chaque texture est dupliquée en plusieurs exemplaires, en plusieurs LOD. Dans le détail, la technique du mip-mapping prend au maximum 33% de mémoire en plus (sans compression). Cela vient du fait qu'en prenant une texture dexu fois plus petite, elle prend 4 fois moins de mémoire : 2 fois moins de pixels en largeur, et 2 fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des LODs, texture de base comprise, prendra X + (X/4) + (X/16) + (X/256) + … Un petit calcul de limite donne 4/3 * X, soit 33% de plus.
===Le cube-mapping===
[[File:Cube mapped reflection example 2.JPG|vignette|Exemple de reflets environnementaux.]]
L''''environnement-mapping''' est une technique de calcul de divers effets graphiques liés à l'environnement, notamment des réflexions. L'idée est de plaquer une texture pré-calculée pour simuler l'effet de l'environnement sur une surface ou un objet 3D. Il en existe plusieurs versions différentes, mais la seule utilisée de nos jours est le ''cube-mapping'', où la texture de l'environnement est plaquée sur un cube, d'où son nom. Le cube en question est utilisé différemment suivant ce que l'on cherche à faire avec le ''cube-mapping''. Les deux utilisations principales sont le rendu du ciel et des décors, et les réflexions sur la surface des objets. Dans les deux cas, l'idée est de précalculer ce que l'on voit du point de vue de la caméra. On place la caméra dans la scène 3D, on place un cube centré sur la caméra, le cube est texturé avec ce que l'on voit de l'environnement depuis la caméra/l'objet de son point de vue.
[[File:Panorama cube map.png|centre|vignette|upright=2|L'illustration montre en premier lieu une ''cubemap'' avec les six faces mises en évidence, puis quel environnement 3D elle permet de simuler, le troisième illustration montrant comment la ''cubemap'' est utilisée pour simuler l'environnement.]]
Le rendu du ciel et des décors lointains dans les jeux vidéo se base sur des '''''skybox''''', à savoir un cube centré sur la caméra, sur lequel on ajoute des textures de ciel ou de décors lointains. Le cube est recouvert par une texture, qui correspond à ce que l'on voit quand on dirige le regard de la caméra vers cette face. Contrairement à ce qu'on pourrait croire, la skybox n'est pas les limites de la scène 3D, les limites du niveau d'un jeu vidéo ou quoique ce soit d'autre de lié à la physique de la scène 3D. La skybox est centrée sur la caméra, elle suit la caméra dans son mouvement. Centrer la skybox sur la caméra permet de simuler des décors très lointains, suffisamment lointain pour qu'on n'ait pas l'illusion de s'en rapprocher en se déplaçant dans la map. De plus, cela évite d'avoir à faire trop de calculs à chaque fois que l'on bouge la caméra. La texture plaquée sur le cube est une texture unique, elle-même découpée en six sous-textures, une par face du cube.
[[File:Skybox example.png|centre|vignette|upright=2|Exemple de Skybox.]]
[[File:Cube mapped reflection example.jpg|vignette|Réflexions calculées par une ''cubemap''.]]
Le ''cube-mapping'' est aussi utilisé pour des reflets. L'idée est de simuler les reflets en plaquant une texture pré-calculée sur l'objet réflecteur. La texture pré-calculée est un dessin de l'environnement qui se reflète sur l'objet, un dessin du reflet à afficher. En la plaquant la texture sur l'objet, on simule ainsi des reflets de l'environnement, mais on ne peut pas calculer d'autres reflets comme les reflets objets mobiles comme les personnages. Et il se trouve que la texture pré-calculée est une ''cubemap''. Pour les environnements ouverts, c'est la ''skybox'' qui est utilisée, ce qui permet de simuler les reflets dans les flaques d'eau ou dans des lacs/océans/autres. Pour les environnements intérieurs, c'est une cubemap spécifique qui utilisée. Par exemple, pour l'intérieur d'une maison, on a une ''cubemap'' par pièce de la maison. Les reflets se calculent en précisant quelle ''cubemap'' appliquer sur l'objet en fonction de la direction du regard.
[[File:Cube map level.png|centre|vignette|Cube map de l'intérieur d'une pièce d'un niveau de jeux vidéo.]]
Toujours est-il que les textures utilisées pour le ''cubemmapping'', appelées des ''cubemaps'', sont en réalité la concaténation de six textures différentes. En mémoire vidéo, la ''cubemap'' est stockée comme six textures les unes à la suite des autres. Lors du rendu, on doit préciser quelle face du cube utiliser, ce qui fait 6 possibilités. On a le même problème qu'avec les niveaux de détail, sauf que ce sont les faces d'une ''cubemap'' qui remplacent les textures de niveaux de détails. L'accès en mémoire doit donc préciser quelle portion de la ''cubemap'' il faut accéder. Et l'accès mémoire se complexifie donc. Surtout que l'accès en question varie beaucoup suivant l'API graphique utilisée, et donc suivant la carte graphique.
Les API 3D assez anciennes ne gérent pas nativement les ''cubemaps'', qui doivent être émulées en logiciel en utilisant six textures différentes. Le pixel shader décide donc quelle ''cubemap'' utiliser, avec quelques calculs sur la direction du regard. L'accès se fait d'une manière assez simple : le shader choisit quelle texture utiliser. Les API 3D récentes gèrent nativement les ''cubemaps''. Dans le cas le plus simple,pour les versions les plus vielles de ces API, les six faces sont numérotées et l'accès à une ''cubemap'' précise quel face utiliser en donnant son numéro. La carte graphique choisit alors automatiquement la bonne texture, mais cela demande de laisser le calcul de la bonne face au pixel shader. D'autres API 3D et cartes graphiques font autrement. Dans les API 3D modenres, les ''cubemap'' sont gérées comme des textures en trois dimensions, adressées avec trois coordonnées u,v,w. La carte graphique utilise ces trois coordonnées de manière à en déduire quelle est la face pertinente, mais aussi les coordonnées u,v dans la texture de la face.
==L'implémentation matérielle du placage de textures==
Pour résumer, la lecture d'un texel demande d'effectuer plusieurs étapes. Dans le cas le plus simple, sans ''mip-mapping'' ou ''cubemapping'', on doit effectuer les étapes suivantes :
* Il faut d'abord normaliser les coordonnées de texture pour qu'elles tombent dans l'intervalle [0,1] en fonction du mode d'adressage désiré.
* Ensuite, les coordonnées u,v doivent être converties en coordonnées entières, ce qui demande une multiplication flottante.
* Enfin, l'adresse finale est calculée à partir des coordonnées entières et en ajoutant l'adresse de base de la texture (et éventuellement avec d'autres calculs arithmétiques suivant le format de la texture).
Tout cela pourrait être fait par le pixel shaders, mais cela implique beaucoup de calculs répétitifs et d'opérations arithmétiques assez lourdes, avec des multiplications flottantes, des additions et des multiplications entières, etc. Faire faire tous ces calculs par les shaders serait couteux en performance, sans compter que les shaders deviendraient plus gros et que cela aurait des conséquences sur le cache d'instruction. De plus, certaines de ces étapes peuvent se faire en parallèle, comme les deux premières, ce qui colle mal avec l'aspect sériel des shaders.
Aussi, les processeurs de shaders incorporent une unité de calcul d'adresse spéciale pour faire ces calculs directement en matériel. L'unité de texture contient au minimum deux circuits : un circuit de calcul d'adresse, et un circuit d'accès à la mémoire. Toute la difficulté tient dans le calcul d'adresse, plus que dans le circuit de lecture. Le calcul d'adresse est conceptuellement réalisé en deux étapes. La première étape qui transforme les coordonnées u,v en coordonnées x,y qui donne le numéro de la ligne et de la colonne du texel dans la texture. La seconde étape prend ces deux coordonnées x,y, l'adresse de la texture, et détermine l'adresse de la tile à lire.
[[File:Unité de texture simple.png|centre|vignette|upright=2|Unité de texture simple]]
===L'implémentation du mip-mapping===
Le ''mip-mapping'' est lui aussi pris en charge par l'unité de calcul d'adresse, car cette technique change l'adresse de base de la texture. La gestion du ''mip-mapping'' est cependant assez complexe. Il est possible de laisser le pixel shader calculer quel niveau de détail utiliser, en fonction de la coordonnée de profondeur z du pixel à afficher. La carte graphique détermine alors automatiquement quelle texture lire, quel niveau de détail, automatiquement. Elle détermine aussi la bonne résolution pour la texture, qui est égal à la résolution de la texture de base, divisée par le niveau de détail. Pour résumer, le niveau de détail est envoyé aux unités de texture, qui s'occupent de calculer l'adresse de base et la résolution adéquates. Quelques calculs arithmétiques simples, donc, qui s'implémentent facilement avec quelques circuits.
Mais une autre méthode laisse la carte graphique déterminer le niveau de détail par elle-même. Dans ce cas, cela demande, outre les deux coordonnées de texture, de calculer la dérivée de ces deux coordonnées dans le sens horizontal et vertical, ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Les quatre dérivées sont les suivantes :
: <math>\frac{du}{dx}</math>, <math>\frac{dv}{dx}</math>, <math>\frac{du}{dy}</math>, <math>\frac{dv}{dy}</math>
Un bon moyen pour obtenir les dérivées demande de regrouper les pixels par groupes de 4 et de faire la différence entre leurs coordonnées de texture respectives. On peut calculer les deux dérivées horizontales en comparant les deux pixels sur la même ligne, et les deux dérivées verticales en comparant les deux pixels sur la même colonne. Mais cela demande de rastériser les pixels par groupes de 4, par ''quads''. Et c'est ce qui est fait sur les cartes graphiques actuelles, qui rastérisent des groupes de 4 pixels à la fois.
[[File:Texture sampler unit with mipmapping.png|centre|vignette|upright=2.0|Unité de texture avec mipmapping.]]
Malheureusement, le calcul exact utilisé pour le choix de la mip-map dépend du GPU considéré et peu de chose est connu quant à ces algorithmes. Il est possible d'inférer le comportement à partir d'observations, mais guère plus. Pour ceux qui veulent en savoir plus, je conseille la lecture de cet article de blog :
* [https://pema.dev/2025/05/09/mipmaps-too-much-detail/ Mipmap selection in too much detail]
===La gestion des accès mémoire===
Enfin, l'unité de texture doit tenir compte du fait que la mémoire vidéo met du temps à lire une texture. En théorie, l'unité de texture ne devrait pas accepter de nouvelle demande de lecture tant que celle en cours n'est pas terminée. Mais faire ainsi demanderait de bloquer tout le pipeline, de l'''input assembler'' au unités de''shaders'', ce qui est tout sauf pratique et nuirait grandement aux performances.
Une solution alternative consiste à mettre en attente les demandes de lectures de texture pendant que la mémoire est occupée. La manière la plus simple d'implémenter des accès mémoire multiples est de les mettre en attente dans une petite mémoire FIFO. Cela implique que les accès mémoire s’exécutent dans l'ordre demandé par le ''shader'' et/ou l'unité de rastérisation, il n'y a pas de réorganisation des accès mémoire ou d’exécution dans le désordre des accès mémoire.
[[File:Texture prefetching.png|centre|vignette|upright=1.5|Accès mémoire simultanés.]]
Évidemment, quand la mémoire FIFO est pleine, le pipeline est alors totalement bloqué. Le rasteriser est prévenu que l'unité de texture ne peut pas accepter de nouvelle lecture de texture. En pratique, la FIFO est généralement d'une taille respectable et permet de mettre en attente beaucoup de demandes de lecture de texture. Il faut de plus noter qu'il y a une FIFO par processeur de ''shader'' sur les cartes graphiques modernes. Quand elle est pleine, le processeur cesse d'exécuter de nouveaux accès mémoire, mais peut continuer à exécuter des ''shaders'' dans les autres unités de calcul, pas besoin de bloquer complétement le pipeline.
===L'intégration du cache de textures===
Il faut noter que les unités de texture incorporent aussi un cache de texture, voire plusieurs. L'intégration des caches de texture avec la mémoire FIFO précédente est quelque peu compliqué, car il faut garantir que les lectures de texture se fassent dans le bon ordre. On ne peut pas exécuter une lecture dans le cache alors que des lectures précédentes sont en attente de lecture en mémoire vidéo. Et cela pose un gros problème : une lecture dans le cache de texture prend quelques dizaines de cycles d'horloge, alors qu'une lecture en mémoire vidéo en prend facilement 400 à 800 cycles, parfois plus. Et cela fait que l'ordre des accès mémoire peut s'inverser.
Prenons par exemple un accès au cache précédé et suivi par deux accès en mémoire vidéo. Le premier démarre au cycle 1, et se termine au cycle numéro 400. L'accès au cache commence au cycle 2 et se termine 20 cycles après, au cycle numéro 22. En clair, la lecture dans le cache s'est terminée avant l'accès mémoire qui le précède. Les textures ne sont donc plus lues dans l'ordre. Et il faut trouver une solution pour éviter cela.
La solution est de retarder les lectures dans le cache tant que tous les accès précédents ne sont pas terminés. Mais pour retarder les lectures en question, il faut d'abord savoir si la lecture atterrit dans le cache ou non, ce qui demande d'accéder au cache. On fait face à un dilemme : on veut retarder les accès au cache, mais les différencier des lectures déclenchant des accès mémoire demande d'accéder au cache en premier lieu. La solution est décrite dans l'article "Prefetching in a Texture Cache Architecture" par Igehy et ses collègues. Elle se base sur deux idées combinées ensemble.
La première idée est de séparer l'accès au cache en deux : une étape qui vérifie si les texels à lire sont dans le cache, et une étape qui accède aux données dans le cache lui-même. Un cache de texture est donc composé de deux circuits principaux. Le premier vérifie la présence des texels dans le cache. Il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Ensuite, en plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
La seconde idée est de retarder l'accès au cache entre les deux phases. La première étape d'un accès mémoire vérifie si la donnée est dans le cache ou non. Puis, on retarde la lecture des données, pour attendre que toutes les lectures précédentes soient terminées. Et enfin, troisième étape : la lecture des texels dans la mémoire cache proprement dite. Les accès mémoire passant par la mémoire vidéo se font de la même manière, à une différence près : la lecture dans le cache est remplacée par la lecture en mémoire vidéo. Tout démarre avec une demande à l'unité de tags, qui vérifie si le texel est dans le cache ou non. Puis on retarde l'accès tant que la mémoire vidéo est occupée, puis on effectue la lecture en mémoire vidéo.
Si ce n'est pas le cas, l'accès mémoire est envoyé à la mémoire vidéo comme précédemment, à savoir qu'il est mis en attente dans une mémoire FIFO, puis envoyé à la mémoire vidéo dès que celle-ci est libre. Mais en sortie de la mémoire, la donnée lue est envoyée dans le cache de texture, par dans l'unité de filtrage. Pour savoir où placer la donnée lue, l'unité de tag a réservé une ligne de cache précise, une adresse bien précise. L'adresse en question est disponible en lisant une autre mémoire FIFO, qui a mis en attente l'adresse en question, en attendant que l'accès mémoire se termine. La donnée est alors écrite dans le cache, puis lue par l'unité de filtrage de textures.
Pour une lecture dans le cache, le déroulement est similaire, mais sans le passage par la mémoire. La lecture fait une demande à l'unité de tag, et celle-ci répond que la donnée est bien dans le cache. Elle place alors l'adresse à lire dans la file d'attente. Une fois que les accès mémoire précédents sont terminés, l'adresse sort de la file d'attente et est envoyée à la mémoire de données. La lecture s'effectue, les texels sont envoyés à l'unité de filtrage de textures. La seule différence avec un ''phased cache'' normal est l'insertion de l'adresse à lire dans une FIFO qui vise à mettre en attente
[[File:Unité de texture avec un cache de texture.png|centre|vignette|upright=2.0|Unité de texture avec un cache de texture]]
Pour résumer, l'implémentation précédente garantit une exécution des lectures dans leur ordre d'arrivée. Et pour cela, elle retarde les lectures dans le cache tant que les lectures en mémoire précédentes ne sont pas terminées. L'accès au cache est plus rapide que l'accès en mémoire vidéo, mais le retard ajouté pour garantir l'ordre des lectures fait que le temps d'accès est très long.
==Le filtrage de textures==
Plaquer des textures sans autre forme de procès ne suffit pas à garantir des graphismes d'une qualité époustouflante. La raison est que les sommets et les texels ne tombent pas tout pile sur un pixel de l'écran : le sommet associé au texel peut être un petit peu trop en haut, ou trop à gauche, etc. Une explication plus concrète fait intervenir les coordonnées de texture. Souvenez-vous que lorsque l'on traduit une coordonnée de texture u,v en coordonnées x,y, on obtient un résultat qui ne tombe pas forcément juste. Souvent, le résultat a une partie fractionnaire. Si celle-ci est non-nulle, cela signifie que le texel/sommet n'est pas situé exactement sur le pixel voulu et que celui-ci est situé à une certaine distance. Concrètement, le pixel tombe entre quatre texels, comme indiqué ci-dessous.
[[File:Filtrage texture.png|centre|vignette|upright=2.0|Position du pixel par rapport aux texels.]]
Pour résoudre ce problème, on doit utiliser différentes techniques d'interpolation, aussi appelées techniques de '''filtrage de texture''', qui visent à calculer la couleur du pixel final en fonction des texels qui l'entourent. Il existe de nombreux types de filtrage de textures, qu'il s'agisse du filtrage linéaire, bilinéaire, trilinéaire, anisotropique et bien d'autres.
Tous ont besoin d'avoir certaines informations qui sont généralement fournies par les circuits de calcul d'adresse. La première est clairement la partie fractionnaire des coordonnées x,y. La seconde est la dérivée de ces deux coordonnées dans le sens horizontal et vertical., ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Toujours est-il que le filtrage de texture est une opération assez lourde, qui demande beaucoup de calculs arithmétiques. On pourrait en théorie le faire dans les pixels shaders, mais le cout en performance serait absolument insoutenable. Aussi, les cartes graphiques intègrent toutes un circuit dédié au filtrage de texture, le ''texture sampler''. Même les plus anciennes cartes graphiques incorporent une unité de filtrage de texture, ce qui nous montre à quel point cette opération est importante.
[[File:Texture unit.png|centre|vignette|upright=2.0|Unité de texture.]]
On peut configurer la carte graphique de manière à ce qu'elle fasse soit du filtrage bilinéaire, soit du filtrage trilinéaire, on peut configurer le niveau de filtrage anisotropique, etc. Cela peut se faire dans les options de la carte graphique, mais cela peut aussi être géré par l'application. La majorité des jeux vidéos permettent de régler cela dans les options. Ces réglages ne concernent pas la texture elle-même, mais plutôt la manière dont l'unité de texture doit fonctionner. Ces réglages sur l''''état de l'unité de texture''' sont mémorisés quelque part, soit dans l'unité de texture elle-même, soit fournies avec la ressource de texture elle-même, tout dépend de la carte graphique. Certaines cartes graphiques mémorisent ces réglages dans les unités de texture ou dans le processeur de commande, et tout changement demande alors de réinitialiser l'état des unités de texture, ce qui prend un peu de temps. D'autres placent ces réglages dans les ressources de texture elles-mêmes, ce qui rend les modifications de configuration plus rapides, mais demande plus de circuits. D'autres cartes graphiques mélangent les deux options, certains réglages étant globaux, d'autres transmis avec la texture. Bref, difficile de faire des généralités, tout dépend du matériel et le pilote de la carte graphique cache tout cela sous le tapis.
Maintenant que cela est dit, voyons quelles sont les différentes méthodes de filtrage de texture et comment la carte graphique fait pour les calculer.
===Le filtrage au plus proche===
La méthode de filtrage la plus simple consiste à colorier avec le texel le plus proche. Cela revient tout simplement à ne pas tenir compte de la partie fractionnaire des coordonnées x,y, ce qui est très simple à implémenter en matériel. C'est ce que l'on appelle le '''filtrage au plus proche''', aussi appelé ''nearest filtering''.
Autant être franc, le résultat est assez pixelisé et peu agréable à l’œil. Par contre, le résultat est très rapide à calculer, vu qu'il ne demande aucun calcul à proprement parler. Elle ne fait pas appel à la parti fractionnaire des coordonnées entières de texture, ni aux dérivées de ces coordonnées. On peut combiner cette technique avec le mip-mapping, ce qui donne un résultat bien meilleur, bien que loin d'être satisfaisant. Au passage, toutes les techniques de filtrage de texture peuvent se combiner avec du mip-mapping, certaines ne pouvant pas faire sans.
[[File:Interpolation-nearest.svg|centre|vignette|Filtrage de texture au plus proche.]]
===Le filtrage linéaire===
Le filtrage le plus simple est le '''filtrage linéaire'''. Il effectue une interpolation linéaire entre deux mip-maps, deux niveaux de détails. Pour comprendre l'idée, nous allons prendre une situation très simple, avec une texture carrée de 512 texels de côté. Le mip-mapping crée plusieurs textures : une de 256 texels de côté, une de 128 texels, une de 64, etc. Maintenant, la texture est sur un objet à une certaine distance de l'écran, vu de face. Le résultat est qu'elle correspond à l'écran à un carré de 300 pixels de côté (pas d'erreur : pixels, pas texels). Dans ce cas, la texture se trouve entre deux mip-maps : celle de 512 pixels de côté, celle de 256. Laquelle choisir ? Le filtrage au plus proche prend la texture de 512 pixels de côté. Le filtrage linéaire lui, fait autrement.
Vu que la texture est entre deux mip-maps, l'idée est de prendre le texel au plus proche dans chaque texture et de faire une sorte de moyenne appelée l'interpolation linéaire. L'interpolation par du principe que la couleur varie entre les deux texels en suivant une fonction affine, illustrée ci-dessous. Ce ne serait évidemment pas le cas dans le monde réel, mais on supposer cela donne une bonne approximation de ce à quoi ressemblerait une texture à plus haute résolution. On peut alors calculer la couleur du pixel par une simple moyenne pondérée par la distance. Le résultat est que les transitions entre deux niveaux de détails sont plus lisses, moins abruptes.
[[File:Lin interp -é.png|centre|vignette|upright=2.0|Interpolation linéaire.]]
===Le filtrage bilinéaire===
Le filtrage bilinéaire effectue une sorte de moyenne pondérée des quatre texels les plus proches du pixel à afficher. Pour cela, rappelez-vous ce qui a été dit plus haut : les coordonnées x,y d'un pixel ont une partie entière et une partie fractionnaire. Le filtrage au plus proche élimine les parties fractionnaires, ce qui donne une coordonnée x,y. Avec le filtrage bilinéaire, on prend les texels de coordonnées (x,y) ; (x+1,y) ; (x,y+1) ; (x+1,y+1), le pixel étant entre ces 4 texels.
Mais le filtrage ne fait pas qu'une simple moyenne, il prend en compte les parties fractionnaires pour faire la moyenne. En effet, le pixel n'est pas au milieu du carré de texel, il est quelque part mais est souvent plus proche d'un texel que des autres. Et il faut donc pondérer la moyenne par les distances aux 4 texels. Pour cela, la moyenne est calculée à partir d'interpolations linéaires. Avec 4 pixels, nous allons devoir calculer la couleur de deux points intermédiaires. La couleur de ces deux points se calcule par interpolation linéaire, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.
[[File:Bilin3.png|centre|vignette|upright=2|Filtrage bilinéaire de texture.]]
Le circuit qui permet de faire l'interpolation bilinéaire est particulièrement simple. On trouve un circuit de chaque pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu, et un pour la transparence. Chacun de ces circuit est composé de sous-circuits chargés d'effectuer une interpolation linéaire, reliés comme suit.
[[File:Texture sampler unit.png|centre|vignette|Unité de filtrage bilinéaire.]]
Vous noterez que le filtrage bilinéaire accède à 4 pixels en même temps. Fort heureusement, les textures sont stockées de manière à ce qu'on puisse charger les 4 pixels en une fois, comme on l'a vu plus haut. Le filtrage bilinéaire a de fortes chances que les 4 pixels filtrés soient dans la même ''tile'', la seule exception étant quand ils sont tout juste sur le bord d'une ''tile''.
: La console de jeu Nintendo 64 n'utilise que trois pixels au lieu de quatre dans son interpolation bilinéaire, qui en devient une interpolation quasi-bilinéaire. La raison derrière ce choix est une question de performances, comme beaucoup de décisions de ce genre. Le résultat est un rendu imparfait de certaines textures.
===Le filtrage trilinéaire===
Avec le filtrage bilinéaire, des discontinuités apparaissent sur certaines surfaces. Par exemple, pensez à une texture de sol : elle est appliquée plusieurs fois sur toute la surface du sol. A une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512*512 à 256*256, ce qui est visible pour un joueur attentif. De telles transitions sont lissées grâce au filtrage linéaire, il n'y a plus qu'à le combiner avec le filtrage bilinéaire. Rien d’incompatible : le premier filtre l'intérieur d'une mip-map, le second combine deux mip-maps.
Le filtrage trilinéaire prend les deux mip-maps les plus proches, fait un filtrage bilinéaire avec chacune, puis fait une « une moyenne » pondérée entre les deux résultats. Le circuit de filtrage trilinéaire existe en plusieurs versions. La plus simple, illustrée ci-dessous, effectue deux filtrages bilinéaires en parallèle, dans deux circuits séparés, puis combine leurs résultats avec un circuit d'interpolation linéaire. Mais ce circuit nécessite de charger 8 texels simultanément. Qui plus est, ces 8 texels ne sont pas consécutifs en mémoire, car ils sont dans deux niveaux de détails/mip-maps différents.
[[File:Parallel trilinear filtering.png|centre|vignette|upright=2.0|Unité de filtrage trilinéaire parallèle.]]
Vu qu'on lit des texels dans deux mip-maps, les texels sont lus en deux fois : 4 texels provenant de la première mip-map, suivis par les 4 texels de l'autre mip-map. Les 4 premiers texels doivent donc être mis en attente dans des registres, en attendant que les 4 autres arrivent. Une amélioration du circuit précédent gère cela en ajoutant des registres. Il lit les 4 premiers texels, les filtre avec une interpolation bilinéaire, et mémorise le résultat dans un registre. Puis, il lit les 4 autres texels, les filtre, et met le résultat dans un second registre. A ce moment là, un circuit d'interpolation linéaire finit le travail. On économise donc un circuit d'interpolation bilinéaire, sans que les performances soient trop impactées.
[[File:Filtrage trilineaire.png|centre|vignette|upright=1.0|Unité de filtrage trilineaire série.]]
Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le temps où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. À chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients, et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.
===Le filtrage anisotrope===
D'autres artefacts peuvent survenir lors de l'application d'une texture, la perspective pouvant déformer les textures et entraîner l'apparition de flou. La raison à cela est que les techniques de filtrage de texture précédentes partent du principe que la texture est vue de face. Prenez une texture carrée, par exemple. Vue de face, elle ressemble à un carré sur l'écran. Mais tournez la caméra, de manière à voir la texture de biais, avec un angle, et vous verrez que la forme de la texture sur l'écran est un trapèze, pas un carré. Cette déformation liée à la perspective n'est pas prise en compte par les méthodes de filtrage de texture précédentes. Pour le dire autrement, les techniques de filtrage précédentes partent du principe que les 4 texels qui entourent un pixel forment un carré, ce qui est vrai si la texture est vue de face, sans angle, mais ne l'est pas si la texture n'est pas perpendiculaire à l'axe de la caméra. Du point de vue de la caméra, les 4 texels forment un trapèze d'autant moins proche d'un carré que l'angle est grand.
Pour corriger cela, les chercheurs ont inventé le '''filtrage anisotrope'''. En fait, je devrais plutôt dire : LES filtrages anisotropes. Il en existe un grand nombre, dont certains ne sont pas utilisés dans les cartes graphiques actuelles, soit car ils trop gourmand en accès mémoires et en calculs pour être efficaces, soit car ils ne sont pas pratiques à mettre en œuvre. Il est très difficile de savoir quelles sont les techniques de filtrage de texture utilisées par les cartes graphiques, qu'elles soient récentes ou anciennes. Beaucoup de ces technologies sont brevetées ou gardées secrètes, et il faudrait vraiment creuser les brevets déposés par les fabricants de GPU pour en savoir plus. Les algorithmes en question seraient de plus difficiles à comprendre, les méthodes mathématiques cachées derrière ces méthodes de filtrage n'étant pas des plus simple.
[[File:Anisotropic filtering en.png|centre|vignette|upright=2|Exemple de filtrage anisotrope.]]
==La compression de textures==
Les textures les plus grosses peuvent aller jusqu'au mébioctet, ce qui est beaucoup. Pour limiter la casse, les textures sont compressées. La '''compression de texture''' réduit la taille des textures, ce qui peut se faire avec ou sans perte de qualité. Elle entraîne souvent une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande. Mais il s'agit là d'une technique très simple, beaucoup plus simple que les techniques que nous allons voir dans cette section. Nous allons voir quelque algorithmes de compression de textures de complexité intermédiaire, mais n'allons pas voir l'état de l'art. Il existe des formats de texture plus récents que ceux qui nous allons aborder, comme l{{'}}''Ericsson Texture Compression'' ou l{{'}}''Adaptive Scalable Texture Compression'', plus complexes et plus efficaces.
Notons que les textures sont compressées dans les fichiers du jeu, mais aussi en mémoire vidéo. Les textures sont décompressées lors de la lecture. Pour cela, la carte graphique contient alors un circuit, capable de décompresser les textures lorsqu'on les lit en mémoire vidéo. Les cartes graphiques supportent un grand nombre de formats de textures, au niveau du circuit de décompression. Du fait que les textures sont décompressées à la volée, les techniques de compression utilisées sont assez particulières. La carte graphique ne peut pas décompresser une texture entière avant de pouvoir l'utiliser dans un ''pixel shader''. A la place, on doit pouvoir lire un morceau de texture, et le décompresser à la volée. On ne peut utiliser les méthodes de compression du JPEG, ou d'autres formats de compression d'image. Ces dernières ne permettent pas de décompresser une image morceau par morceau.
Pour permettre une décompression/compression à la volée, les textures sont des textures tilées, généralement découpées en tiles de 4 * 4 texels. Les ''tiles'' sont compressées indépendamment les unes des autres. Et surtout, avec ou sans compression, la position des tiles en mémoire ne change pas. On trouve toujours une tile tous les T octets, peu importe que la tile soit compressée ou non. Par contre, une tile compressée n'occupera pas T octets, mais moins, là où une tile compressée occupera la totalité des T octets. En clair, compresser une tile fait qu'il y a des vides entre deux tiles dans al mémoire vidéo, mais ne change rien à leur place en mémoire vidéo qui est prédéterminée, peu importe que la texture soit compressée ou non. L'intérêt de la compression de textures n'est pas de réduire la taille de la texture en mémoire vidéo, mais de réduire la quantité de données à lire/écrire en mémoire vidéo. Au lieu de lire T octets pour une tile non-compressée, on pourra en lire moins.
===La palette indicée et la technique de ''Vector quantization''===
La technique de compression des textures la plus simple est celle de la '''palette indicée''', que l'on a entraperçue dans le chapitre sur les cartes d'affichage. La technique de '''''vector quantization''''' peut être vue comme une amélioration de la palette, qui travaille non pas sur des texels, mais sur des ''tiles''. À l'intérieur de la carte graphique, on trouve une table qui stocke toutes les ''tiles'' possibles. Chaque ''tile'' se voit attribuer un numéro, et la texture sera composé d'une suite de ces numéros. Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l’embarqué utilisent ce genre de compression.
===Les algorithmes de ''Block Truncation coding''===
La première technique de compression élaborée est celle du '''''Block Truncation Coding''''', qui ne marche que pour les images en niveaux de gris. Le BTC ne mémorise que deux niveaux de gris par ''tile'', que nous appellerons couleur 1 et couleur 2, les deux niveaux de gris n'étant pas le même d'une ''tile'' à l'autre. Chaque pixel d'une ''tile'' est obligatoirement colorié avec un de ces niveaux de gris. Pour chaque pixel d'une ''tile'', on mémorise sa couleur avec un bit : 0 pour couleur 1, et 1 pour couleur 2. Chaque ''tile'' est donc codée par deux entiers, qui codent chacun un niveau de gris, et une suite de bits pour les pixels proprement dit. Le circuit de décompression est alors vraiment très simple, comme illustré ci-dessous.
[[File:Block Truncation coding.jpg|centre|vignette|upright=2.0|Block Truncation coding.]]
La technique du BTC peut être appliquée non pas du des niveaux de gris, mais pour chaque composante Rouge, Vert et Bleu. Dans ces conditions, chaque ''tile'' est séparée en trois sous-''tiles'' : un sous-bloc pour la composante verte, un autre pour le rouge, et un dernier pour le bleu. Cela prend donc trois fois plus de place en mémoire que le BTC pur, mais cela permet de gérer les images couleur.
===Le format de compression S3TC / DXTC===
L'algorithme de '''Color Cell Compression''', ou CCC, améliore le BTC pour qu'il gère des couleurs autre que des niveaux de gris. Ce CCC remplace les deux niveaux de gris par deux couleurs. Une ''tile'' est donc codée avec un entier 32 bits par couleur, et une suite de bits pour les pixels. Le circuit de décompression est identique à celui utilisé pour le BTC.
[[File:Color Cell Compression.jpg|centre|vignette|Color Cell Compression.]]
[[File:Dxt1-memory-layout.png|vignette|Dxt1 et ''color cell compression''.]]
Le format de compression de texture utilisé de base par Direct X, le DXTC, est une version amliorée de l'algorithme précédent. Il est décliné en plusieurs versions : DXTC1, DXTC2, etc. La première version du DXTC est une sorte d'amélioration du CCC : il ajoute une gestion minimale de transparence, et découpe la texture à compresser en ''tiles'' de 4 pixels de côté. La différence, c'est que la couleur finale d'un texel est un mélange des deux couleurs attribuée au bloc. Pour indiquer comment faire ce mélange, on trouve deux bits de contrôle par texel.
Si jamais la couleur 1 < couleur2, ces deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (2 * Couleur1 + Couleur2) / 3
* 11 = (Couleur1 + 2 * Couleur2) / 3
Sinon, les deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (Couleur1 + Couleur2) / 2
* 11 = Transparent
[[File:DXTC.jpg|centre|vignette|DXTC.]]
Le circuit de décompression du DXTC ressemble alors à ceci :
[[File:Circuit de décompression du DXTC.jpg|centre|vignette|upright=2.0|Circuit de décompression du DXTC.]]
===Les format DXTC 2, 3, 4 et 5 : l'ajout de la transparence===
Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3, lui-même replacé par le DXT4 et par le DXT5. Dans le DXT3, la transparence fait son apparition. Pour cela, on ajoute 64 bits par ''tile'' pour stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1.
[[File:Dxt23-memory-layout.png|centre|vignette|Dxt 2 et 3.]]
Dans le DXT4 et le DXT5, la méthode utilisée pour compresser les couleurs l'est aussi pour les valeurs de transparence. L'information de transparence est stockée par un en-tête contenant deux valeurs de transparence, le tout suivi d'une matrice qui attribue trois bits à chaque texel. En fonction de la valeur des trois bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1.
[[File:Dxt45-memory-layout.png|centre|vignette|Dxt 4 et 5.]]
===Le format de compression PVRTC===
Passons maintenant à un format de compression de texture un peu moins connu, mais pourtant omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle travaille surtout dans les cartes graphiques embarquées. Ses cartes se trouvent notamment dans l'ipad, l'iPhone, et bien d'autres smartphones actuels.
Avec le PVRTC, les textures sont encore une fois découpées en ''tiles'' de 4 texels par 4, mais la ressemblance avec le DXTC s’arrête là. Chacque ''tile'' est codée avec :
* une couleur codée sur 16 bits ;
* une couleur codée sur 15 bits ;
* 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
* et un bit de modulation, qui permet de configurer l’interprétation des bits de mélange.
Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4*4.
==Annexe : le ''normal-mapping'' hardware==
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, parlons un peu du ''normal mapping'' et de son implémentation dans les unités de texture. Pour rappel, 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. Et pour comprendre quel est le rapport avec les textures, nous allons devoir faire quelques rappels sur l'éclairage par pixel.
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
L'éclairage se fait en utilisant de nombreux calculs, qu'on a détaillé dans le chapitre sur les bases du rendu 3D. Ceux-ci utilisent la normale d'un sommet, à savoir un vecteur orienté à la verticale de la surface. En théorie, il y a une normale par sommet, ce qui fait que les calculs d'éclairage doivent se faire au niveau géométrique, avant la rastérisation. Mais pour obtenir un éclairage de meilleure qualité, il y a des techniques qui calcule l'éclairage d'une scène 3D pixel par pixel.
Une première technique d'éclairage par pixel interpole les normales lors de l'étape de rastérisation. On obtient alors un éclairage de Phong. Une autre solution est celle du ''normal mapping''. Le '''''normal mapping''''' précalcule les normales d'une surface dans une texture, appelée la ''normal map''. L'éclairage est alors réalisé par un pixel shader, qui lit les normales depuis cette texture et fait les calculs d'éclairage avec.
Notons que l'accès à la ''normal map'' se fait comme pour n'importe quelle texture : les texels sont lus en mémoire vidéo, puis le filtrage de texture est appliqué, et enfin le tout est envoyé au ''pixel shader''. En théorie, le filtrage effectue une sorte d'interpolation des normales automatique, ce qui permet de trouver la normale à un pixel précis, même s'il n'est pas sur un texel.
La ''normal map'' est une texture, donc. Mais ce n'est pas une texture comme une autre. Elle mémorise un vecteur pour chaque texel, pas une couleur. Il est possible d'utiliser les formats de textures vus plus haut, même si cela ne donne pas de bons résultats. Après tout, un vecteur est codé par trois coordonnées x,y,z, il est possible de coder chacune avec un octet et de packager cela dans une texture RGB classique. La coordonnée x va dans la composante Rouge, la coordonnée Y dans la composante Bleu, et la coordonnée z dans la composante z.
Le problème est que le rendu final n'est pas très beau. Et les opérations de filtrage ne donnent pas de très bons résultats. Pour remédier à ces problèmes, des formats de texture spécialisés pour les ''normal map'' ont été inventés. Le premier d'entre eux est le '''3Dc''', aussi appelé BC5 pour ''Block Compression 5''. Il utilise un octet par texel, au lieu de trois avec une texture RGB normale non-compressée.
Avec le 3Dc, les trois coordonnées des vecteurs ne sont pas mémorisées, seules deux d'entre elles le sont. En effet, les normales sont des vecteurs ''normalisés'', c'est à dire que leur longueur vaut 1, par construction. Vu que la taille est connue à l'avance, on peut en déduire la coordonnée z à partir des coordonnées x et y, avec l'usage du théorème de Pythagore. Par définition, <math>z^2 = 1 - x^2 - y^2</math>. Le calcul peut être fait dans le ''pixel shader'' sans problème.
Pour le reste, la compression de la ''normal map'' ressemble un peu à celle des textures. La ''normal map'' est découpée en tiles de 4 par 4 texels de côté, chacune étant encodée en tenant compte de certaines redondances.
==Annexe : les ''shadowmap'' hardware==
Les anciens GPU, notamment la Geforce FX, avaient des fonctionnalités spécifiques pour le calcul des ombres. Dans la plupart des jeux vidéos de l'époque, et même de maintenant, les ombres sont calculées avec la technique des ''shadowmap''. L'idée est assez simple sur le principe : un pixel est dans l'ombre quand il est invisible depuis une source de lumière.
L'idée est que le rendu est réalisé en plusieurs passes, avec une passe par source de lumière et une passe finale pour calculer l'image finale. Nous allons expliquer la technique avec une seule source de lumière, et allons utiliser l'exemple de la scène ci-dessous.
[[File:7fin.png|centre|vignette|Scène 3D d'exemple.]]
===La technique du ''shadowmapping''===
[[File:2shadowmap.png|vignette|Résultat de la première passe : ''shadowmap''..]]
La première passe rend l'image depuis le point de vue de la source de lumière. Cette première passe ne rend pas les couleurs de la scène, elle ne s'intéresse qu'à la profondeur des pixels. Le résultat est que l'image ne rend que le tampon de profondeur. Celui-ci est ensuite réutilisé comme texture pour la passe suivante. La texture en question est appelée la '''''shadownmap'''''.
La perspective utilisée, ainsi que le ''view frustrum'', dépend de la source de lumière. Pour une source de lumière qui émet un cône de lumière, le ''view frustrum'' de l'image rendue doit contenir tout le cone de lumière, et doit coller le plus possible à celui-ci. Pour une source directionnelle, comme le soleil, une perspective orthographique est utilisée.
La seconde passe rend l'image du point de vue de la caméra, pour rendre l'image finale. Elle rend l'image finale, qui est composée de pixels, chacun ayant une position à l'écran x,y, et une profondeur z. Les coordonnées sont transformées pour obtenir la position de ce pixel depuis le point de vue de la caméra. Une simple multiplication de matrice suffit, rien de bien compliqué, un shader peut le faire.
[[File:5failed.png|vignette|Résultat du test des comparaisons.]]
Après cette étape, on a alors les coordonnées x,y,z de ce pixel du point de vue de la caméra, et la ''shadowmap''. Il est alors possible d'accéder à la ''shadowmap'' au même endroit, à la même place que le pixel testé, aux mêmes coordonnées x,y. Si la profondeur du pixel est supérieure à celle de la shadowmap au même endroit, alors le pixel est situé derrière la surface visible, donc est dans l'ombre. Sinon, il n'est pas dans l'ombre. Le même procédé est répété sur chaque pixel de l'écran.
===Les optimisations hardware du ''shadowmapping''===
La technique des ''shadowmap'' demande donc de calculer une texture ''shadowmap'', puis de lire celle-ci et de faire des comparaisons de profondeur. Les GPU comme la Geforce FX intégraient du matériel dans les unités de texture pour faciliter ce travail. Les unités de texture pouvaient lire les ''shadowmap'', et faire la comparaison de profondeur toutes seules, elles avaient des circuits pour. Il suffisait de leur fournir le pixel à tester, ses coordonnées x,y,z, et l'adresse de la ''shadowmap''. Les unités de texture renvoyaient alors un résultat valant 0 ou 1 : 1 si le pixel est dans l'ombre, 0 sinon.
Elles pouvaient même effectuer du filtrage de texture sur les ''shadowmap''. Mais le filtrage était différent de celui utilisé sur les autres textures : moyenner des valeurs de profondeur ne marche pas bien. Elles utilisaient des techniques de filtrage différentes : elles faisaient les tests de comparaison, puis faisaient la moyenne des résultats. Ainsi, pour du filtrage bilinéaire, elles lisaient 4 texels dans la ''shadowmap'', puis faisaient 4 tests de comparaison, et moyennaient les 4 résultats.
==Annexe : les textures virtuelles==
Les '''textures virtuelles''' sont une optimisation des textures normales, qui visent à accélérer le rendu de terrains de grande taille. Imaginez par exemple un monde assez ouvert, comme un environnement en forêt ou en montagne, avec une grande distance de visibilité. Avec de tels terrains, le "sol" est recouvert par une texture de sol unique qui recouvre tout le terrain. Elle ne se répète pas, est de très grande taille, et peut parfois recouvrir toute la map ! Mais il n'y a pas assez de mémoire vidéo pour mémoriser la texture toute entière. La seule solution est la suivante : une partie de la texture est placée en mémoire vidéo, le reste est soit placé en mémoire RAM ou sur le disque dur.
Pour cela, le moteur de jeu utilise une optimisation ingénieuse, basée sur une observation assez basique : une bonne partie de la texture est visible, mais le reste est caché par des arbres, des habitations ou d'autres obstacles. Une optimisation possible de ne garder en mémoire vidéo que les portions visibles de la texture, pas les portions cachées. Une autre optimisation mélange textures virtuelles et ''mip-mapping''. L'idée est que pour les portions lointaines d'une texture, la texture utilisée est une ''mip-map'' de basse résolution. L'idée est alors de ne charger que la ''mip-map'' adéquate, pas les autres niveaux de détail. En clair, la texture de base n'est pas chargée en mémoire vidéo, mais la ''mip-map'' basse résolution l'est.
===Une texture à deux niveaux===
L'implémentation des textures virtuelles découpe les méga-textures en ''tiles'', en morceaux rectangulaires de taille modeste. En clair, le terrain est découpé en morceau rectangulaires/carrés. Seules les tiles nécessaires sont chargées en mémoire vidéo, pas les autres. Par exemple, les ''tiles'' non-visibles ne sont pas placées en mémoire vidéo, seules les ''tiles'' visibles le sont. De même, il y a une ''tile'' par niveau de mip-map : seul la tile correspondant le niveau adéquat est en mémoire vidéo, les autres niveaux de détail ne sont pas chargés. On peut faire une analogie avec la mémoire virtuelle, où les données sont découpées en pages, qui sont chargées en mémoire RAM à la demande, suivant les besoins, les données pouvant être swappées sur le disque dur si elles sont peu utilisées. Sauf qu'ici, il s'agit de textures qui sont découpées en pages chargées à la demande en mémoire vidéo, depuis la RAM système.
Une texture virtuelle est en réalité un système à deux niveaux : une liste de ''tiles'' et les ''tiles'' elles-mêmes. La liste de ''tiles'' est appelée un '''atlas de texture''', c'est un peu l'équivalent de la ''tilemap'' pour le rendu 2D. Rendre une texture demande de calculer quelle ''tile'' contient le texel à afficher, consulter la ''tile'' en question, puis récupérer le texel adéquat dans cette ''tile''. La ''tile'' est donc une texture, mais la texture à charger est choisie parmi un ensemble, qui est ici l'atlas de texture.
===L'implémentation : logicielle versus matérielle===
Les textures virtuelles ont été utilisées pour la première fois par les jeux Rage 1 et 2 d'IdSoftware, et quelques jeux ultérieurs comme DOOM 2016. IdSoftware les appelait des '''''mega-textures'''''. L'optimisation permettait des gains en performance assez impressionnants. Le jeu Rage 1 utilisait une texture carrée unique de 128k pixels de côté pour rendre le terrain. En théorie, une telle texture devrait prendre 64 giga-octets, mais le jeu tournait correctement avec 512 méga-octets de RAM, poussivement avec seulement 256 méga-octets de RAM.
De nos jours, les textures virtuelles sont supportées par beaucoup de jeux vidéos, les moteurs les plus courants gèrent de telles textures de manière logicielles. Mais quelques GPU récents supportent les textures virtuelles. Sur les GPU récents, l'atlas de texture est géré nativement par le matériel. Le GPU choisit quelle ''tile'', quelle texture choisir pour rendre le texel adéquat. Pour cela, le GPU calcule quelle ''tile'' charger, consulte l'atlas de texture, et lit la texture de ''tile'' adéquate.
Mais l'implémentation sur les GPU récents a de nombreuses limitations. La limitation la plus importante est que la taille des textures virtuelles ne peut pas dépasser la taille d'une texture normale, soit 32768 pixels de côté pour une texture carrée environ sur les GPU de 2020. De plus, le chargement d'une ''tile'' est très lent. En clair, dès qu'on veut changer de niveau de mip-map pour une tile, ou dès qu'une tile devient visible, le chargement de la tile peut facilement prendre plusieurs centaines de millisecondes. Le filtrage de texture est très complexe avec des textures virtuelles, ce qui fait que le filtrage de texture virtuelle est souvent soumis à des limitations que les textures normales n'ont pas, notamment pour le filtrage anisotropique.
{{NavChapitre | book=Les cartes graphiques
| prev=Le rasterizeur
| prevText=Le rasterizeur
| next=Les Render Output Target
| nextText=Les Render Output Target
}}{{autocat}}
sku8zac9yfboyk4rhrpgyngtzt3s4x9
763406
763405
2026-04-10T16:55:02Z
Mewtow
31375
763406
wikitext
text/x-wiki
[[File:Texture mapping.png|vignette|''Texture mapping'']]
Les '''textures''' sont des images que l'on va plaquer sur la surface d'un objet, du papier peint en quelque sorte. Les cartes graphiques supportent divers formats de textures, qui indiquent comment les pixels de l'image sont stockés en mémoire : RGB, RGBA, niveaux de gris, etc. Une texture est donc composée de "pixels", comme toute image numérique. Pour bien faire la différence entre les pixels d'une texture, et les pixels de l'écran, les pixels d'une texture sont couramment appelés des ''texels''.
==Le placage de textures inverse==
Pour rappel, plaquer une texture sur un objet consiste à attribuer un texel à chaque sommet, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture.
Dans les faits, on n'utilise pas de coordonnées entières de ce type. Les coordonnées de texture sont deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. Le nom donnée à cette technique de description des coordonnées de texture s'appelle l''''''UV Mapping'''''.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Les API 3D modernes gèrent des textures en trois dimensions, ce qui ajoute une troisième coordonnée de texture notée w. Dans ce qui va suivre, nous allons passer les textures en trois dimensions sous silence. Elles ne sont pas très utilisées, la quasi-totalité des jeux vidéo et applications 3D utilisant des textures en deux dimensions. Par contre, le matériel doit gérer les textures 3D, ce qui le rend plus complexe que prévu. Il faut ajouter quelques circuits pour, de quoi gérer la troisième coordonnée de texture, etc.
Lors de la rastérisation, chaque fragment se voit attribuer un sommet, et donc la coordonnée de texture qui va avec. Si un pixel est situé pile sur un sommet, la coordonnée de texture de ce sommet est attribuée au pixel. Si ce n'est pas le cas, la coordonnée de texture finale est interpolée à partir des coordonnées des trois sommets du triangle rastérisé. L'interpolation en question a lieu dans l'étape de rastérisation, comme nous l'avons vu dans le chapitre précédent. Le fait qu'il y ait une interpolation fait que les coordonnées du pixel gagent à être des nombres flottants. On pourrait faire une interpolation avec des coordonnées de texture entières, mais les arrondis et autres imprécisions de calcul donneraient un résultat graphiquement pas terrible, et empêcheraient d'utiliser les techniques de filtrage de texture que nous verrons dans ce chapitre.
À partir de ces coordonnées de texture, la carte graphique calcule l'adresse du texel qui correspond, et se charge de le lire. Et toute la magie a lieu dans ce calcul d'adresse, qui part de coordonnées de texture flottante, pour arriver à une adresse mémoire. Le calcul de l'adresse du texel se fait en plusieurs étapes, que nous allons voir ci-dessous. La première étape convertit les coordonnées flottantes en coordonnées entières, qui disent à quel ligne et colonne se trouve le texel voulu dans la texture. L'étape suivante transforme ces coordonnées x,y entières en adresse mémoire.
===La normalisation des coordonnées===
J'ai dit plus haut que les coordonnées de texture sont des coordonnées flottantes, comprises entre 0 et 1. Mais il faut savoir que les pixels shaders peuvent modifier celles-ci pour mettre en œuvre certains effets graphiques. Et le résultat peut alors se retrouver en-dehors de l'intervalle 0,1. C'est quelque chose de voulu et qui est traité par la carte graphique automatiquement, sans que ce soit une erreur. Au contraire, la manière dont la carte graphique traite cette situation permet d'implémenter des effets graphiques comme des textures en damier ou en miroir.
[[File:Clamp tile.jpg|vignette|Clamp tile]]
Il existe globalement trois méthodes très simples pour gérer cette situation, qui sont appelés des '''modes d'adressage de texture'''.
* La première méthode est de faire en sorte que le résultat sature. Si une coordonnée est inférieur à 0, alors on la remplace par un zéro. Si elle est supérieure à 1, on la ramène à 1. Avec cette méthode, tout se passe comme si les bords de la texture étaient étendus et remplissaient tout l'espace autour de la texture. Le tout est illustré ci-dessous. Ce mode d'accès aux textures est appelé le '''''clamp'''''.
* Une autre solution retire la partie entière de la coordonnée, elle coupe tout ce qui dépasse 1. Pour le dire autrement, elle calcule le résultat modulo 1 de la coordonnée. Le résultat est que tout se passe comme si la texture était répétée à l'infini et qu'elle pavait le plan.
* Une autre méthode remplit les coordonnées qui sortent de l’intervalle 0,1 avec une couleur préétablie, configurée par le programmeur.
===La conversion des coordonnées de textures flottantes en adresse mémoire===
Une fois la normalisation effectuée, les coordonnées de texture sont utilisées pour lire le texel voulu. Pour cela, les coordonnées de texte sont transformées en adresse mémoire, adresse qui pointe sur le texel ayant ces cordonnées. Pour cela, la première étape est de transformer les coordonnées flottantes u,v en coordonnées entières x,y qui pointent sur un texel. Pour cela, il suffit de multiplier les coordonnées flottantes u,v par la résolution de la texture accédée. Pour un écran de résolution <math>\text{height,width}</math>, le calcul est le suivant :
: <math>x = u \times \text{width}</math>
: <math>y = v \times \text{height}</math>
Le résultat est un nombre avec une partie entière et une partie fractionnaire. La partie entière des deux coordonnées donne la position x,y voulue, et la partie fractionnaire est conservée pour le filtrage de textures, mais passons cela sous silence pour le moment.
La seconde étape prend les coordonnées entières x,y et calcule l'adresse mémoire du texel. L'adresse dépend de la position de la texture en mémoire, précisément de son début, son premier texel, mais aussi de la position du texel par rapport au début de la texture. Et calculer cette position intra-texture dépend de la manière dont les texels sont stockés en mémoire.
====Les textures naïves====
Les programmeurs qui lisent ce cours s'attendent certainement à ce que la texture soit stockée en mémoire ligne par ligne, ou colonne par colonne. Cela veut dire que le premier pixel en partant d'en haut à gauche est stocké en premier, puis celui immédiatement à sa droite, puis celui encore à droite, et ainsi de suite. Une fois qu'on arrive à la fin d'une ligne, on passe à la ligne suivante, en-dessous. Cette organisation ligne par ligne s'appele l'organisation '''''row major order'''''. On peut faire pareil, mais colonne par colonne, ce qui donne le '''''column major order'''''.
[[File:Speicheranordnung Feld.svg|centre|vignette|upright=2|Row et column major order.]]
Maintenant, supposons que la texture commence à l'adresse <math>A_\text{texture}</math>, qui est l'adresse du premier texel. La texture a une résolution de <math>\text{width}</math> texels de large et <math>\text{height}</math> texels de haut. Par définition, les coordonnées X et Y des texels commencent à 0, ce qui fait que le pixel en haut à gauche a les coordonnées 0,0.
L'adresse du pixel se calcule comme suit :
: <math>A_\text{pixel} = A_\text{texture} + (\text{taille d'une ligne en octets} \times Y) + (\text{taille d'un texel en octets} \times X)</math>
La taille d'un pixel en mémoire est notée T. La taille d'une ligne en mémoire est de <math>width \times T</math>, par définition, vu qu'elle fait <math>width</math> texels. On a donc :
: <math>A_\text{pixel} = A_\text{texture} + (width \times T \times Y) + (T \times X)</math>
La formule se réécrit comme suit :
: <math>A_\text{pixel} = A_\text{texture} + T \times (width \times Y + X)</math>
Le calcul d'adresse est donc assez simple. Malheureusement, les textures ne sont pas stockées de cette manière en mémoire vidéo. En effet, elle se marie mal avec les opérations de filtrage de texture que nous allons voir dans ce qui suit. Le filtrage d'un texel dépend de ses voisins du dessus et du dessous. Le fait que la texture n'est pas forcément parcourue ligne par ligne fait que stocker une texture ligne par ligne n'est pas l'idéal.
De même, les textures sont déformées par la perspective. L'affichage de la texture ne se fait alors pas ligne par ligne, mais en parcourant la texture en diagonale, l'angle de la diagonale correspondant approximativement à l'angle que fait la verticale de la texture avec le regard. Vu qu'on ne connait pas à l'avance l'angle que fera la diagonale de parcours, on doit ruser.
====Les textures tilées====
Une première solution à ce problème est celle des '''textures tilées'''. Avec ces textures, l'image de la texture est découpée en ''tiles'', des rectangles ou en carrés de taille fixe, généralement des carrés de 4 pixels de côté. Les tiles ont une largeur et une longueur égales, afin de simplifier les calculs : on divise X et Y par le même nombre. De plus, leur largeur et leur longueur sont une puissance de deux, afin de simplifier les calculs d'adresse. Les ''tiles'' sont alors mémorisée les unes après les autres dans le fichier de la texture.
[[File:Texture tilée.png|centre|vignette|upright=2|Texture tilée]]
La formule de calcul d'adresse vue plus haut doit être adaptée pour tenir compte des tiles. Pour cela, il faut remplacer la taille d'un texel par la taille d'une tile, et que la largeur de la texture soit exprimée en nombre de tiles. De plus, on doit adapter les coordonnées des texels pour donner des coordonnées de tile. Généralement, les tiles sont des carrés de N pixels de côté, ce qui fait qu'on peut regrouper les lignes et les colonnes par paquets de N. Il suffit donc de diviser Y et X pour obtenir les coordonnées de la tile, de même que la larguer. La formule pour calculer la position de la énième tile est alors la suivante :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + \text{Taille mémoire d'une tile} \times \left( {\text{Width} \over N} \times {Y \over N} + {X \over N} \right)</math>
On peut réécrire le tout comme suit :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + K \times \left( {Y \over N} + X \right)</math>, avec K une constante connue à la compilation des shaders.
Vu que les tiles sont carrées avec une largeur qui est une puissance de deux, la multiplication par la taille d'une tile en mémoire se simplifie : on passe d'une multiplication entière à des décalages de bits. Même chose pour le calcul de l'adresse de la tile à partir des coordonnées x,y : ils impliquent des divisions par une puissance de deux, qui deviennent de simples décalages.
La position d'un pixel dans une tile dépend du format de la texture, mais peut se calculer avec quelques calculs arithmétiques simples. Dans les cas les plus simples, les pixels sont mémorisés ligne par ligne, ou colonne par colonne. Mais ce n'est pas systématiquement le cas. Toujours est-il que les calculs pour déterminer l'adresse sont simples, et ne demandent que quelques additions ou multiplications. Mais avec les formats de texture utilisés actuellement, les tiles sont chargées en entier dans le cache de texture, sans compter que diverses techniques de compression viennent mettre le bazar, comme on le verra dans la suite de cours.
Un avantage de l'organisation en tiles est qu'elle se marie bien avec le parcours des textures. On peut parcourir une texture dans tous les sens, horizontal, vertical, ou diagonal, on sait que les prochains pixels ont de fortes chances d'être dans la même tile. Si on rentre dans une tile par la gauche en haut, on a encore quelques pixels à parcourir dans la tile, par exemple. De même, le filtrage de textures est facilité. On verra dans ce qui va suivre que le filtrage de texture a besoin de lire des blocs de 4 texels, des carrés de 2 pixels de côté. Avec l'organisation en tile, on est certain que les 4 texels seront dans la même tile, sauf s'ils ont le malheur d'être tout au bord d'une tile. Ce dernier cas est assez rare, et il l'est d'autant plus que les tiles sont grandes. Enfin, un dernier avantage est que les tiles sont généralement assez petites pour tenir tout entier dans une ligne de cache. Le cache de texture est donc utilisé à merveille, ce qui rend les accès aux textures plus rapides.
====Les textures basées sur des ''z-order curves''====
Les formats de textures théoriquement optimaux utilisent une '''''Z-order curve''''', illustrée ci-dessous. L'idée est de découper la texture en quatre rectangles identiques, et de stocker ceux-ci les uns à la suite des autres. L'intérieur de ces rectangles est lui aussi découpé en quatre rectangles, et ainsi de suite. Au final, l'ordre des pixels en mémoire est celui illustré ci-dessous.
[[File:Z-CURVE.svg|centre|vignette|upright=2|Construction d'une ''Z-order curve''.]]
Les texels sont stockés les uns à la suite des autres dans la mémoire, en suivant l'ordre donnée par la ''Z-order curve''. Le calcul d'adresse calcule la position du texel en mémoire, par rapport au début de la texture, et ajoute l'adresse du début de la texture. Mais tout le défi est de calculer la position d'un texel en mémoire, à partir des coordonnées x,y. Le calcul peut sembler très compliqué, mais il n'en est rien. Le calcul demande juste de regarder les bits des deux coordonnées et de les combiner d'une manière particulièrement simple. Il suffit de placer le bit de poids fort de la coordonnée x, suivi de celui de la coordonnée y, et de faire ainsi de suite en passant aux bits suivants.
[[File:Zcurve45bits.png|centre|vignette|upright=1.5|Calcul de la position d'un élément dans une ''Z-order curve'' à partir des coordonnées x et y.]]
L'avantage d'une telle organisation est que la textures est découpées en ''tiles'' rectangulaires d'une certaine taille, elles-mêmes découpées en ''tiles'' plus petites, etc. Et il se trouve que cette organisation est parfaite pour le cache de texture. L'idéal pour le cache de texture est de charger une ''tile'' complète dans le cache de textures. Quand on accède à un texel, on s'assure que la ''tile'' complète soit chargée. Mais cela demande de connaitre à l'avance la taille d'une ''tile''. Les formats de texture fournissent généralement une ''tile'' carré de 4 pixels de côté, mais cela donnerait un cache trop petit pour être vraiment utile. Avec cette méthode, on s'assure qu'il y ait une ''tile'' avec la taille optimale. Les ''tiles'' étant découpées en ''tiles'' plus petites, elles-mêmes découpées, et ainsi de suite, on s'assure que la texture est découpées en ''tiles'' de taille variées. Il y aura au moins une ''tile'' qui rentrera tout pile dans le cache.
==Les techniques de rendu à textures multiples==
Nous venons de voir comment une texture est plaquée sur un objet 3D, ou une surface comme un sol. Pour résumer, le calcul de l'adresse d'un texel prend la position du texel par rapport au début de la texture, et ajoute l'adresse du début de la texture. L'adresse mémoire de la texture est connue au moment où le pilote de la carte graphique place la texture dans la mémoire vidéo, et cette information est transmise au matériel par l'intermédiaire du processeur de commande, puis passée aux processeurs de shaders et à l'unité de texture. Le tout est couplé à d'autres informations, la plus importante étant la ''taille de la texture en octets'', pour éviter de déborder lors des accès à la texture.
Néanmoins, il s'agit là du cas le plus simple. Certaines techniques de rendu demandent de choisir la texture à plaquer parmi un ensemble de plusieurs textures. Les techniques en question sont assez variées et n'ont pas grand chose en commun. Les plus connues sont le ''mip-mapping'', le ''cube-mapping'' et les textures virtuelles. Le ''mip-mapping'' sert à filtrer les textures, chose qu'on expliquera plus tard, le ''cube-mapping'' sert à simuler des réflexions sur un objet en plaquant une texture de l'environnement dessus, les textures virtuelles sont une optimisation pour les textures des terrains de grande taille. Mais malgré leurs différences, elles demandent de choisir quelle texture plaquer entre plusieurs textures de base. En clair, l'adresse de base de la texture varie selon la situation. Voyons-les dans le détail.
===Le mip-mapping===
Le '''mip-mapping''' a pour but de légèrement améliorer les graphismes des objets lointains, tout en rendant les calculs de texture plus rapides. Formellement, le ''mip-mapping'' est une technique de filtrage de texture, mais nous l'abordons maintenant car elle est surtout liée au calcul d'adresse. Les unités de texture ont des circuits de filtrage de texture séparés des circuits de ''mip-mapping'' et de calcul d'adresse, d'où le fait que nous en parlons séparément.
Le problème résolu par le ''mip-mapping'' est le rendu des textures lointaines. Si une texture est plaquée sur un objet lointain, une bonne partie des détails est invisible pour l'utilisateur. Un pixel de l'écran est associé à plusieurs texels. Idéalement, la carte graphiques devrait lire tous ces texels et en faire une sorte de moyenne pondérée, pour calculer la couleur finale du pixel. Mais dans les faits, ce serait très gourmand et compliqué à implémenter en hardware. Une solution serait de ne garder que quelque texels, mais cela a tendance à créer des artefacts visuels (les textures affichées ont tendance à pixeliser). Le ''mip-mapping'' permet de réduire ces deux problèmes en même temps en précalculant cette moyenne pondérée pour des distances prédéfinies.
L'idée est d'utiliser plusieurs exemplaires d'une même texture à des résolutions différentes, chaque exemplaire étant adapté à une certaine distance. Par exemple, une texture sera stocké avec un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre de 128 * 128 et ainsi de suite jusqu’à un dernier exemplaire de 32 * 32 pixel. Chaque exemplaire correspond à un '''niveau de détail''', aussi appelé ''Level Of Detail'' (abrévié en LOD). La résolution utilisée diminue d'autant plus que l'objet est situé loin de la caméra. Les objets proches seront rendus avec la texture 512*512, ceux plus lointains seront rendus avec la texture de résolution 256*256, les textures 128*128 seront utilisées encore plus loin, et ainsi de suite jusqu'aux objets les plus lointains qui sont rendus avec la texture la plus petite de 32*32.
[[File:MipMap Example STS101.jpg|centre|vignette|upright=2|Exemples de mip-maps.]]
Le ''mip-mapping'' améliore grandement la qualité d'image. L'image d'exemple ci-dessous le montre assez bien.
[[File:Mipmapping example.png|centre|vignette|upright=2|Exemple de mipmapping.]]
Pour faciliter les calculs d'adresse, les LOD d'une même texture sont stockées les uns après les autres en mémoire (dans un tableau, comme diraient les programmeurs). Ainsi, pas besoin de se souvenir de la position en mémoire de chaque LOD : l'adresse de la texture de base, et quelques astuces arithmétiques suffisent. Prenons le cas où la texture de base a une taille L. le premier exemplaire est à l'adresse 0, le second niveau de détail est à l'adresse L, le troisième à l'adresse L + L/4, le suivant à l'adresse L + L/4 + L/16, et ainsi de suite. Le calcul d'adresse demande juste connaître le niveau de détails souhaité et l'adresse de base de la texture. Le niveau de détail voulu est calculé par les pixel shaders, en fonction de la coordonnée de profondeur du pixel à traiter.
Évidemment, cette technique consomme de la mémoire vidéo, vu que chaque texture est dupliquée en plusieurs exemplaires, en plusieurs LOD. Dans le détail, la technique du mip-mapping prend au maximum 33% de mémoire en plus (sans compression). Cela vient du fait qu'en prenant une texture dexu fois plus petite, elle prend 4 fois moins de mémoire : 2 fois moins de pixels en largeur, et 2 fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des LODs, texture de base comprise, prendra X + (X/4) + (X/16) + (X/256) + … Un petit calcul de limite donne 4/3 * X, soit 33% de plus.
===Le cube-mapping===
[[File:Cube mapped reflection example 2.JPG|vignette|Exemple de reflets environnementaux.]]
L''''environnement-mapping''' est une technique de calcul de divers effets graphiques liés à l'environnement, notamment des réflexions. L'idée est de plaquer une texture pré-calculée pour simuler l'effet de l'environnement sur une surface ou un objet 3D. Il en existe plusieurs versions différentes, mais la seule utilisée de nos jours est le ''cube-mapping'', où la texture de l'environnement est plaquée sur un cube, d'où son nom. Le cube en question est utilisé différemment suivant ce que l'on cherche à faire avec le ''cube-mapping''. Les deux utilisations principales sont le rendu du ciel et des décors, et les réflexions sur la surface des objets. Dans les deux cas, l'idée est de précalculer ce que l'on voit du point de vue de la caméra. On place la caméra dans la scène 3D, on place un cube centré sur la caméra, le cube est texturé avec ce que l'on voit de l'environnement depuis la caméra/l'objet de son point de vue.
[[File:Panorama cube map.png|centre|vignette|upright=2|L'illustration montre en premier lieu une ''cubemap'' avec les six faces mises en évidence, puis quel environnement 3D elle permet de simuler, le troisième illustration montrant comment la ''cubemap'' est utilisée pour simuler l'environnement.]]
Le rendu du ciel et des décors lointains dans les jeux vidéo se base sur des '''''skybox''''', à savoir un cube centré sur la caméra, sur lequel on ajoute des textures de ciel ou de décors lointains. Le cube est recouvert par une texture, qui correspond à ce que l'on voit quand on dirige le regard de la caméra vers cette face. Contrairement à ce qu'on pourrait croire, la skybox n'est pas les limites de la scène 3D, les limites du niveau d'un jeu vidéo ou quoique ce soit d'autre de lié à la physique de la scène 3D. La skybox est centrée sur la caméra, elle suit la caméra dans son mouvement. Centrer la skybox sur la caméra permet de simuler des décors très lointains, suffisamment lointain pour qu'on n'ait pas l'illusion de s'en rapprocher en se déplaçant dans la map. De plus, cela évite d'avoir à faire trop de calculs à chaque fois que l'on bouge la caméra. La texture plaquée sur le cube est une texture unique, elle-même découpée en six sous-textures, une par face du cube.
[[File:Skybox example.png|centre|vignette|upright=2|Exemple de Skybox.]]
[[File:Cube mapped reflection example.jpg|vignette|Réflexions calculées par une ''cubemap''.]]
Le ''cube-mapping'' est aussi utilisé pour des reflets. L'idée est de simuler les reflets en plaquant une texture pré-calculée sur l'objet réflecteur. La texture pré-calculée est un dessin de l'environnement qui se reflète sur l'objet, un dessin du reflet à afficher. En la plaquant la texture sur l'objet, on simule ainsi des reflets de l'environnement, mais on ne peut pas calculer d'autres reflets comme les reflets objets mobiles comme les personnages. Et il se trouve que la texture pré-calculée est une ''cubemap''. Pour les environnements ouverts, c'est la ''skybox'' qui est utilisée, ce qui permet de simuler les reflets dans les flaques d'eau ou dans des lacs/océans/autres. Pour les environnements intérieurs, c'est une cubemap spécifique qui utilisée. Par exemple, pour l'intérieur d'une maison, on a une ''cubemap'' par pièce de la maison. Les reflets se calculent en précisant quelle ''cubemap'' appliquer sur l'objet en fonction de la direction du regard.
[[File:Cube map level.png|centre|vignette|Cube map de l'intérieur d'une pièce d'un niveau de jeux vidéo.]]
Toujours est-il que les textures utilisées pour le ''cubemmapping'', appelées des ''cubemaps'', sont en réalité la concaténation de six textures différentes. En mémoire vidéo, la ''cubemap'' est stockée comme six textures les unes à la suite des autres. Lors du rendu, on doit préciser quelle face du cube utiliser, ce qui fait 6 possibilités. On a le même problème qu'avec les niveaux de détail, sauf que ce sont les faces d'une ''cubemap'' qui remplacent les textures de niveaux de détails. L'accès en mémoire doit donc préciser quelle portion de la ''cubemap'' il faut accéder. Et l'accès mémoire se complexifie donc. Surtout que l'accès en question varie beaucoup suivant l'API graphique utilisée, et donc suivant la carte graphique.
Les API 3D assez anciennes ne gérent pas nativement les ''cubemaps'', qui doivent être émulées en logiciel en utilisant six textures différentes. Le pixel shader décide donc quelle ''cubemap'' utiliser, avec quelques calculs sur la direction du regard. L'accès se fait d'une manière assez simple : le shader choisit quelle texture utiliser. Les API 3D récentes gèrent nativement les ''cubemaps''. Dans le cas le plus simple,pour les versions les plus vielles de ces API, les six faces sont numérotées et l'accès à une ''cubemap'' précise quel face utiliser en donnant son numéro. La carte graphique choisit alors automatiquement la bonne texture, mais cela demande de laisser le calcul de la bonne face au pixel shader. D'autres API 3D et cartes graphiques font autrement. Dans les API 3D modenres, les ''cubemap'' sont gérées comme des textures en trois dimensions, adressées avec trois coordonnées u,v,w. La carte graphique utilise ces trois coordonnées de manière à en déduire quelle est la face pertinente, mais aussi les coordonnées u,v dans la texture de la face.
==L'implémentation matérielle du placage de textures==
Pour résumer, la lecture d'un texel demande d'effectuer plusieurs étapes. Dans le cas le plus simple, sans ''mip-mapping'' ou ''cubemapping'', on doit effectuer les étapes suivantes :
* Il faut d'abord normaliser les coordonnées de texture pour qu'elles tombent dans l'intervalle [0,1] en fonction du mode d'adressage désiré.
* Ensuite, les coordonnées u,v doivent être converties en coordonnées entières, ce qui demande une multiplication flottante.
* Enfin, l'adresse finale est calculée à partir des coordonnées entières et en ajoutant l'adresse de base de la texture (et éventuellement avec d'autres calculs arithmétiques suivant le format de la texture).
Tout cela pourrait être fait par le pixel shaders, mais cela implique beaucoup de calculs répétitifs et d'opérations arithmétiques assez lourdes, avec des multiplications flottantes, des additions et des multiplications entières, etc. Faire faire tous ces calculs par les shaders serait couteux en performance, sans compter que les shaders deviendraient plus gros et que cela aurait des conséquences sur le cache d'instruction. De plus, certaines de ces étapes peuvent se faire en parallèle, comme les deux premières, ce qui colle mal avec l'aspect sériel des shaders.
Aussi, les processeurs de shaders incorporent une unité de calcul d'adresse spéciale pour faire ces calculs directement en matériel. L'unité de texture contient au minimum deux circuits : un circuit de calcul d'adresse, et un circuit d'accès à la mémoire. Toute la difficulté tient dans le calcul d'adresse, plus que dans le circuit de lecture. Le calcul d'adresse est conceptuellement réalisé en deux étapes. La première étape qui transforme les coordonnées u,v en coordonnées x,y qui donne le numéro de la ligne et de la colonne du texel dans la texture. La seconde étape prend ces deux coordonnées x,y, l'adresse de la texture, et détermine l'adresse de la tile à lire.
[[File:Unité de texture simple.png|centre|vignette|upright=2|Unité de texture simple]]
===L'implémentation du mip-mapping===
Le ''mip-mapping'' est lui aussi pris en charge par l'unité de calcul d'adresse, car cette technique change l'adresse de base de la texture. La gestion du ''mip-mapping'' est cependant assez complexe. Il est possible de laisser le pixel shader calculer quel niveau de détail utiliser, en fonction de la coordonnée de profondeur z du pixel à afficher. La carte graphique détermine alors automatiquement quelle texture lire, quel niveau de détail, automatiquement. Elle détermine aussi la bonne résolution pour la texture, qui est égal à la résolution de la texture de base, divisée par le niveau de détail. Pour résumer, le niveau de détail est envoyé aux unités de texture, qui s'occupent de calculer l'adresse de base et la résolution adéquates. Quelques calculs arithmétiques simples, donc, qui s'implémentent facilement avec quelques circuits.
Mais une autre méthode laisse la carte graphique déterminer le niveau de détail par elle-même. Dans ce cas, cela demande, outre les deux coordonnées de texture, de calculer la dérivée de ces deux coordonnées dans le sens horizontal et vertical, ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Les quatre dérivées sont les suivantes :
: <math>\frac{du}{dx}</math>, <math>\frac{dv}{dx}</math>, <math>\frac{du}{dy}</math>, <math>\frac{dv}{dy}</math>
Un bon moyen pour obtenir les dérivées demande de regrouper les pixels par groupes de 4 et de faire la différence entre leurs coordonnées de texture respectives. On peut calculer les deux dérivées horizontales en comparant les deux pixels sur la même ligne, et les deux dérivées verticales en comparant les deux pixels sur la même colonne. Mais cela demande de rastériser les pixels par groupes de 4, par ''quads''. Et c'est ce qui est fait sur les cartes graphiques actuelles, qui rastérisent des groupes de 4 pixels à la fois.
[[File:Texture sampler unit with mipmapping.png|centre|vignette|upright=2.0|Unité de texture avec mipmapping.]]
Malheureusement, le calcul exact utilisé pour le choix de la mip-map dépend du GPU considéré et peu de chose est connu quant à ces algorithmes. Il est possible d'inférer le comportement à partir d'observations, mais guère plus. Pour ceux qui veulent en savoir plus, je conseille la lecture de cet article de blog :
* [https://pema.dev/2025/05/09/mipmaps-too-much-detail/ Mipmap selection in too much detail]
===La gestion des accès mémoire===
Enfin, l'unité de texture doit tenir compte du fait que la mémoire vidéo met du temps à lire une texture. En théorie, l'unité de texture ne devrait pas accepter de nouvelle demande de lecture tant que celle en cours n'est pas terminée. Mais faire ainsi demanderait de bloquer tout le pipeline, de l'''input assembler'' au unités de''shaders'', ce qui est tout sauf pratique et nuirait grandement aux performances.
Une solution alternative consiste à mettre en attente les demandes de lectures de texture pendant que la mémoire est occupée. La manière la plus simple d'implémenter des accès mémoire multiples est de les mettre en attente dans une petite mémoire FIFO. Cela implique que les accès mémoire s’exécutent dans l'ordre demandé par le ''shader'' et/ou l'unité de rastérisation, il n'y a pas de réorganisation des accès mémoire ou d’exécution dans le désordre des accès mémoire.
[[File:Texture prefetching.png|centre|vignette|upright=1.5|Accès mémoire simultanés.]]
Évidemment, quand la mémoire FIFO est pleine, le pipeline est alors totalement bloqué. Le rasteriser est prévenu que l'unité de texture ne peut pas accepter de nouvelle lecture de texture. En pratique, la FIFO est généralement d'une taille respectable et permet de mettre en attente beaucoup de demandes de lecture de texture. Il faut de plus noter qu'il y a une FIFO par processeur de ''shader'' sur les cartes graphiques modernes. Quand elle est pleine, le processeur cesse d'exécuter de nouveaux accès mémoire, mais peut continuer à exécuter des ''shaders'' dans les autres unités de calcul, pas besoin de bloquer complétement le pipeline.
===L'intégration du cache de textures===
Il faut noter que les unités de texture incorporent aussi un cache de texture, voire plusieurs. L'intégration des caches de texture avec la mémoire FIFO précédente est quelque peu compliqué, car il faut garantir que les lectures de texture se fassent dans le bon ordre. On ne peut pas exécuter une lecture dans le cache alors que des lectures précédentes sont en attente de lecture en mémoire vidéo. Et cela pose un gros problème : une lecture dans le cache de texture prend quelques dizaines de cycles d'horloge, alors qu'une lecture en mémoire vidéo en prend facilement 400 à 800 cycles, parfois plus. Et cela fait que l'ordre des accès mémoire peut s'inverser.
Prenons par exemple un accès au cache précédé et suivi par deux accès en mémoire vidéo. Le premier démarre au cycle 1, et se termine au cycle numéro 400. L'accès au cache commence au cycle 2 et se termine 20 cycles après, au cycle numéro 22. En clair, la lecture dans le cache s'est terminée avant l'accès mémoire qui le précède. Les textures ne sont donc plus lues dans l'ordre. Et il faut trouver une solution pour éviter cela.
La solution est de retarder les lectures dans le cache tant que tous les accès précédents ne sont pas terminés. Mais pour retarder les lectures en question, il faut d'abord savoir si la lecture atterrit dans le cache ou non, ce qui demande d'accéder au cache. On fait face à un dilemme : on veut retarder les accès au cache, mais les différencier des lectures déclenchant des accès mémoire demande d'accéder au cache en premier lieu. La solution est décrite dans l'article "Prefetching in a Texture Cache Architecture" par Igehy et ses collègues. Elle se base sur deux idées combinées ensemble.
La première idée est de séparer l'accès au cache en deux : une étape qui vérifie si les texels à lire sont dans le cache, et une étape qui accède aux données dans le cache lui-même. Un cache de texture est donc composé de deux circuits principaux. Le premier vérifie la présence des texels dans le cache. Il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Ensuite, en plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
La seconde idée est de retarder l'accès au cache entre les deux phases. La première étape d'un accès mémoire vérifie si la donnée est dans le cache ou non. Puis, on retarde la lecture des données, pour attendre que toutes les lectures précédentes soient terminées. Et enfin, troisième étape : la lecture des texels dans la mémoire cache proprement dite. Les accès mémoire passant par la mémoire vidéo se font de la même manière, à une différence près : la lecture dans le cache est remplacée par la lecture en mémoire vidéo. Tout démarre avec une demande à l'unité de tags, qui vérifie si le texel est dans le cache ou non. Puis on retarde l'accès tant que la mémoire vidéo est occupée, puis on effectue la lecture en mémoire vidéo.
Si ce n'est pas le cas, l'accès mémoire est envoyé à la mémoire vidéo comme précédemment, à savoir qu'il est mis en attente dans une mémoire FIFO, puis envoyé à la mémoire vidéo dès que celle-ci est libre. Mais en sortie de la mémoire, la donnée lue est envoyée dans le cache de texture, par dans l'unité de filtrage. Pour savoir où placer la donnée lue, l'unité de tag a réservé une ligne de cache précise, une adresse bien précise. L'adresse en question est disponible en lisant une autre mémoire FIFO, qui a mis en attente l'adresse en question, en attendant que l'accès mémoire se termine. La donnée est alors écrite dans le cache, puis lue par l'unité de filtrage de textures.
Pour une lecture dans le cache, le déroulement est similaire, mais sans le passage par la mémoire. La lecture fait une demande à l'unité de tag, et celle-ci répond que la donnée est bien dans le cache. Elle place alors l'adresse à lire dans la file d'attente. Une fois que les accès mémoire précédents sont terminés, l'adresse sort de la file d'attente et est envoyée à la mémoire de données. La lecture s'effectue, les texels sont envoyés à l'unité de filtrage de textures. La seule différence avec un ''phased cache'' normal est l'insertion de l'adresse à lire dans une FIFO qui vise à mettre en attente
[[File:Unité de texture avec un cache de texture.png|centre|vignette|upright=2.0|Unité de texture avec un cache de texture]]
Pour résumer, l'implémentation précédente garantit une exécution des lectures dans leur ordre d'arrivée. Et pour cela, elle retarde les lectures dans le cache tant que les lectures en mémoire précédentes ne sont pas terminées. L'accès au cache est plus rapide que l'accès en mémoire vidéo, mais le retard ajouté pour garantir l'ordre des lectures fait que le temps d'accès est très long.
==Le filtrage de textures==
Plaquer des textures sans autre forme de procès ne suffit pas à garantir des graphismes d'une qualité époustouflante. La raison est que les sommets et les texels ne tombent pas tout pile sur un pixel de l'écran : le sommet associé au texel peut être un petit peu trop en haut, ou trop à gauche, etc. Une explication plus concrète fait intervenir les coordonnées de texture. Souvenez-vous que lorsque l'on traduit une coordonnée de texture u,v en coordonnées x,y, on obtient un résultat qui ne tombe pas forcément juste. Souvent, le résultat a une partie fractionnaire. Si celle-ci est non-nulle, cela signifie que le texel/sommet n'est pas situé exactement sur le pixel voulu et que celui-ci est situé à une certaine distance. Concrètement, le pixel tombe entre quatre texels, comme indiqué ci-dessous.
[[File:Filtrage texture.png|centre|vignette|upright=2.0|Position du pixel par rapport aux texels.]]
Pour résoudre ce problème, on doit utiliser différentes techniques d'interpolation, aussi appelées techniques de '''filtrage de texture''', qui visent à calculer la couleur du pixel final en fonction des texels qui l'entourent. Il existe de nombreux types de filtrage de textures, qu'il s'agisse du filtrage linéaire, bilinéaire, trilinéaire, anisotropique et bien d'autres.
Tous ont besoin d'avoir certaines informations qui sont généralement fournies par les circuits de calcul d'adresse. La première est clairement la partie fractionnaire des coordonnées x,y. La seconde est la dérivée de ces deux coordonnées dans le sens horizontal et vertical., ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Toujours est-il que le filtrage de texture est une opération assez lourde, qui demande beaucoup de calculs arithmétiques. On pourrait en théorie le faire dans les pixels shaders, mais le cout en performance serait absolument insoutenable. Aussi, les cartes graphiques intègrent toutes un circuit dédié au filtrage de texture, le ''texture sampler''. Même les plus anciennes cartes graphiques incorporent une unité de filtrage de texture, ce qui nous montre à quel point cette opération est importante.
[[File:Texture unit.png|centre|vignette|upright=2.0|Unité de texture.]]
On peut configurer la carte graphique de manière à ce qu'elle fasse soit du filtrage bilinéaire, soit du filtrage trilinéaire, on peut configurer le niveau de filtrage anisotropique, etc. Cela peut se faire dans les options de la carte graphique, mais cela peut aussi être géré par l'application. La majorité des jeux vidéos permettent de régler cela dans les options. Ces réglages ne concernent pas la texture elle-même, mais plutôt la manière dont l'unité de texture doit fonctionner. Ces réglages sur l''''état de l'unité de texture''' sont mémorisés quelque part, soit dans l'unité de texture elle-même, soit fournies avec la ressource de texture elle-même, tout dépend de la carte graphique. Certaines cartes graphiques mémorisent ces réglages dans les unités de texture ou dans le processeur de commande, et tout changement demande alors de réinitialiser l'état des unités de texture, ce qui prend un peu de temps. D'autres placent ces réglages dans les ressources de texture elles-mêmes, ce qui rend les modifications de configuration plus rapides, mais demande plus de circuits. D'autres cartes graphiques mélangent les deux options, certains réglages étant globaux, d'autres transmis avec la texture. Bref, difficile de faire des généralités, tout dépend du matériel et le pilote de la carte graphique cache tout cela sous le tapis.
Maintenant que cela est dit, voyons quelles sont les différentes méthodes de filtrage de texture et comment la carte graphique fait pour les calculer.
===Le filtrage au plus proche===
La méthode de filtrage la plus simple consiste à colorier avec le texel le plus proche. Cela revient tout simplement à ne pas tenir compte de la partie fractionnaire des coordonnées x,y, ce qui est très simple à implémenter en matériel. C'est ce que l'on appelle le '''filtrage au plus proche''', aussi appelé ''nearest filtering''.
Autant être franc, le résultat est assez pixelisé et peu agréable à l’œil. Par contre, le résultat est très rapide à calculer, vu qu'il ne demande aucun calcul à proprement parler. Elle ne fait pas appel à la parti fractionnaire des coordonnées entières de texture, ni aux dérivées de ces coordonnées. On peut combiner cette technique avec le mip-mapping, ce qui donne un résultat bien meilleur, bien que loin d'être satisfaisant. Au passage, toutes les techniques de filtrage de texture peuvent se combiner avec du mip-mapping, certaines ne pouvant pas faire sans.
[[File:Interpolation-nearest.svg|centre|vignette|Filtrage de texture au plus proche.]]
===Le filtrage linéaire===
Le filtrage le plus simple est le '''filtrage linéaire'''. Il effectue une interpolation linéaire entre deux mip-maps, deux niveaux de détails. Pour comprendre l'idée, nous allons prendre une situation très simple, avec une texture carrée de 512 texels de côté. Le mip-mapping crée plusieurs textures : une de 256 texels de côté, une de 128 texels, une de 64, etc. Maintenant, la texture est sur un objet à une certaine distance de l'écran, vu de face. Le résultat est qu'elle correspond à l'écran à un carré de 300 pixels de côté (pas d'erreur : pixels, pas texels). Dans ce cas, la texture se trouve entre deux mip-maps : celle de 512 pixels de côté, celle de 256. Laquelle choisir ? Le filtrage au plus proche prend la texture de 512 pixels de côté. Le filtrage linéaire lui, fait autrement.
Vu que la texture est entre deux mip-maps, l'idée est de prendre le texel au plus proche dans chaque texture et de faire une sorte de moyenne appelée l'interpolation linéaire. L'interpolation par du principe que la couleur varie entre les deux texels en suivant une fonction affine, illustrée ci-dessous. Ce ne serait évidemment pas le cas dans le monde réel, mais on supposer cela donne une bonne approximation de ce à quoi ressemblerait une texture à plus haute résolution. On peut alors calculer la couleur du pixel par une simple moyenne pondérée par la distance. Le résultat est que les transitions entre deux niveaux de détails sont plus lisses, moins abruptes.
[[File:Lin interp -é.png|centre|vignette|upright=2.0|Interpolation linéaire.]]
===Le filtrage bilinéaire===
Le filtrage bilinéaire effectue une sorte de moyenne pondérée des quatre texels les plus proches du pixel à afficher. Pour cela, rappelez-vous ce qui a été dit plus haut : les coordonnées x,y d'un pixel ont une partie entière et une partie fractionnaire. Le filtrage au plus proche élimine les parties fractionnaires, ce qui donne une coordonnée x,y. Avec le filtrage bilinéaire, on prend les texels de coordonnées (x,y) ; (x+1,y) ; (x,y+1) ; (x+1,y+1), le pixel étant entre ces 4 texels.
Mais le filtrage ne fait pas qu'une simple moyenne, il prend en compte les parties fractionnaires pour faire la moyenne. En effet, le pixel n'est pas au milieu du carré de texel, il est quelque part mais est souvent plus proche d'un texel que des autres. Et il faut donc pondérer la moyenne par les distances aux 4 texels. Pour cela, la moyenne est calculée à partir d'interpolations linéaires. Avec 4 pixels, nous allons devoir calculer la couleur de deux points intermédiaires. La couleur de ces deux points se calcule par interpolation linéaire, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.
[[File:Bilin3.png|centre|vignette|upright=2|Filtrage bilinéaire de texture.]]
Le circuit qui permet de faire l'interpolation bilinéaire est particulièrement simple. On trouve un circuit de chaque pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu, et un pour la transparence. Chacun de ces circuit est composé de sous-circuits chargés d'effectuer une interpolation linéaire, reliés comme suit.
[[File:Texture sampler unit.png|centre|vignette|Unité de filtrage bilinéaire.]]
Vous noterez que le filtrage bilinéaire accède à 4 pixels en même temps. Fort heureusement, les textures sont stockées de manière à ce qu'on puisse charger les 4 pixels en une fois, comme on l'a vu plus haut. Le filtrage bilinéaire a de fortes chances que les 4 pixels filtrés soient dans la même ''tile'', la seule exception étant quand ils sont tout juste sur le bord d'une ''tile''.
: La console de jeu Nintendo 64 n'utilise que trois pixels au lieu de quatre dans son interpolation bilinéaire, qui en devient une interpolation quasi-bilinéaire. La raison derrière ce choix est une question de performances, comme beaucoup de décisions de ce genre. Le résultat est un rendu imparfait de certaines textures.
===Le filtrage trilinéaire===
Avec le filtrage bilinéaire, des discontinuités apparaissent sur certaines surfaces. Par exemple, pensez à une texture de sol : elle est appliquée plusieurs fois sur toute la surface du sol. A une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512*512 à 256*256, ce qui est visible pour un joueur attentif. De telles transitions sont lissées grâce au filtrage linéaire, il n'y a plus qu'à le combiner avec le filtrage bilinéaire. Rien d’incompatible : le premier filtre l'intérieur d'une mip-map, le second combine deux mip-maps.
Le filtrage trilinéaire prend les deux mip-maps les plus proches, fait un filtrage bilinéaire avec chacune, puis fait une « une moyenne » pondérée entre les deux résultats. Le circuit de filtrage trilinéaire existe en plusieurs versions. La plus simple, illustrée ci-dessous, effectue deux filtrages bilinéaires en parallèle, dans deux circuits séparés, puis combine leurs résultats avec un circuit d'interpolation linéaire. Mais ce circuit nécessite de charger 8 texels simultanément. Qui plus est, ces 8 texels ne sont pas consécutifs en mémoire, car ils sont dans deux niveaux de détails/mip-maps différents.
[[File:Parallel trilinear filtering.png|centre|vignette|upright=2.0|Unité de filtrage trilinéaire parallèle.]]
Vu qu'on lit des texels dans deux mip-maps, les texels sont lus en deux fois : 4 texels provenant de la première mip-map, suivis par les 4 texels de l'autre mip-map. Les 4 premiers texels doivent donc être mis en attente dans des registres, en attendant que les 4 autres arrivent. Une amélioration du circuit précédent gère cela en ajoutant des registres. Il lit les 4 premiers texels, les filtre avec une interpolation bilinéaire, et mémorise le résultat dans un registre. Puis, il lit les 4 autres texels, les filtre, et met le résultat dans un second registre. A ce moment là, un circuit d'interpolation linéaire finit le travail. On économise donc un circuit d'interpolation bilinéaire, sans que les performances soient trop impactées.
[[File:Filtrage trilineaire.png|centre|vignette|upright=1.0|Unité de filtrage trilineaire série.]]
Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le temps où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. À chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients, et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.
===Le filtrage anisotrope===
D'autres artefacts peuvent survenir lors de l'application d'une texture, la perspective pouvant déformer les textures et entraîner l'apparition de flou. La raison à cela est que les techniques de filtrage de texture précédentes partent du principe que la texture est vue de face. Prenez une texture carrée, par exemple. Vue de face, elle ressemble à un carré sur l'écran. Mais tournez la caméra, de manière à voir la texture de biais, avec un angle, et vous verrez que la forme de la texture sur l'écran est un trapèze, pas un carré. Cette déformation liée à la perspective n'est pas prise en compte par les méthodes de filtrage de texture précédentes. Pour le dire autrement, les techniques de filtrage précédentes partent du principe que les 4 texels qui entourent un pixel forment un carré, ce qui est vrai si la texture est vue de face, sans angle, mais ne l'est pas si la texture n'est pas perpendiculaire à l'axe de la caméra. Du point de vue de la caméra, les 4 texels forment un trapèze d'autant moins proche d'un carré que l'angle est grand.
Pour corriger cela, les chercheurs ont inventé le '''filtrage anisotrope'''. En fait, je devrais plutôt dire : LES filtrages anisotropes. Il en existe un grand nombre, dont certains ne sont pas utilisés dans les cartes graphiques actuelles, soit car ils trop gourmand en accès mémoires et en calculs pour être efficaces, soit car ils ne sont pas pratiques à mettre en œuvre. Il est très difficile de savoir quelles sont les techniques de filtrage de texture utilisées par les cartes graphiques, qu'elles soient récentes ou anciennes. Beaucoup de ces technologies sont brevetées ou gardées secrètes, et il faudrait vraiment creuser les brevets déposés par les fabricants de GPU pour en savoir plus. Les algorithmes en question seraient de plus difficiles à comprendre, les méthodes mathématiques cachées derrière ces méthodes de filtrage n'étant pas des plus simple.
[[File:Anisotropic filtering en.png|centre|vignette|upright=2|Exemple de filtrage anisotrope.]]
==La compression de textures==
Les textures les plus grosses peuvent aller jusqu'au mébioctet, ce qui est beaucoup. Pour limiter la casse, les textures sont compressées. La '''compression de texture''' réduit la taille des textures, ce qui peut se faire avec ou sans perte de qualité. Elle entraîne souvent une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande. Mais il s'agit là d'une technique très simple, beaucoup plus simple que les techniques que nous allons voir dans cette section. Nous allons voir quelque algorithmes de compression de textures de complexité intermédiaire, mais n'allons pas voir l'état de l'art. Il existe des formats de texture plus récents que ceux qui nous allons aborder, comme l{{'}}''Ericsson Texture Compression'' ou l{{'}}''Adaptive Scalable Texture Compression'', plus complexes et plus efficaces.
Notons que les textures sont compressées dans les fichiers du jeu, mais aussi en mémoire vidéo. Les textures sont décompressées lors de la lecture. Pour cela, la carte graphique contient alors un circuit, capable de décompresser les textures lorsqu'on les lit en mémoire vidéo. Les cartes graphiques supportent un grand nombre de formats de textures, au niveau du circuit de décompression. Du fait que les textures sont décompressées à la volée, les techniques de compression utilisées sont assez particulières. La carte graphique ne peut pas décompresser une texture entière avant de pouvoir l'utiliser dans un ''pixel shader''. A la place, on doit pouvoir lire un morceau de texture, et le décompresser à la volée. On ne peut utiliser les méthodes de compression du JPEG, ou d'autres formats de compression d'image. Ces dernières ne permettent pas de décompresser une image morceau par morceau.
Pour permettre une décompression/compression à la volée, les textures sont des textures tilées, généralement découpées en tiles de 4 * 4 texels. Les ''tiles'' sont compressées indépendamment les unes des autres. Et surtout, avec ou sans compression, la position des tiles en mémoire ne change pas. On trouve toujours une tile tous les T octets, peu importe que la tile soit compressée ou non. Par contre, une tile compressée n'occupera pas T octets, mais moins, là où une tile compressée occupera la totalité des T octets. En clair, compresser une tile fait qu'il y a des vides entre deux tiles dans al mémoire vidéo, mais ne change rien à leur place en mémoire vidéo qui est prédéterminée, peu importe que la texture soit compressée ou non. L'intérêt de la compression de textures n'est pas de réduire la taille de la texture en mémoire vidéo, mais de réduire la quantité de données à lire/écrire en mémoire vidéo. Au lieu de lire T octets pour une tile non-compressée, on pourra en lire moins.
===La palette indicée et la technique de ''Vector quantization''===
La technique de compression des textures la plus simple est celle de la '''palette indicée''', que l'on a entraperçue dans le chapitre sur les cartes d'affichage. La technique de '''''vector quantization''''' peut être vue comme une amélioration de la palette, qui travaille non pas sur des texels, mais sur des ''tiles''. À l'intérieur de la carte graphique, on trouve une table qui stocke toutes les ''tiles'' possibles. Chaque ''tile'' se voit attribuer un numéro, et la texture sera composé d'une suite de ces numéros. Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l’embarqué utilisent ce genre de compression.
===Les algorithmes de ''Block Truncation coding''===
La première technique de compression élaborée est celle du '''''Block Truncation Coding''''', qui ne marche que pour les images en niveaux de gris. Le BTC ne mémorise que deux niveaux de gris par ''tile'', que nous appellerons couleur 1 et couleur 2, les deux niveaux de gris n'étant pas le même d'une ''tile'' à l'autre. Chaque pixel d'une ''tile'' est obligatoirement colorié avec un de ces niveaux de gris. Pour chaque pixel d'une ''tile'', on mémorise sa couleur avec un bit : 0 pour couleur 1, et 1 pour couleur 2. Chaque ''tile'' est donc codée par deux entiers, qui codent chacun un niveau de gris, et une suite de bits pour les pixels proprement dit. Le circuit de décompression est alors vraiment très simple, comme illustré ci-dessous.
[[File:Block Truncation coding.jpg|centre|vignette|upright=2.0|Block Truncation coding.]]
La technique du BTC peut être appliquée non pas du des niveaux de gris, mais pour chaque composante Rouge, Vert et Bleu. Dans ces conditions, chaque ''tile'' est séparée en trois sous-''tiles'' : un sous-bloc pour la composante verte, un autre pour le rouge, et un dernier pour le bleu. Cela prend donc trois fois plus de place en mémoire que le BTC pur, mais cela permet de gérer les images couleur.
===Le format de compression S3TC / DXTC===
L'algorithme de '''Color Cell Compression''', ou CCC, améliore le BTC pour qu'il gère des couleurs autre que des niveaux de gris. Ce CCC remplace les deux niveaux de gris par deux couleurs. Une ''tile'' est donc codée avec un entier 32 bits par couleur, et une suite de bits pour les pixels. Le circuit de décompression est identique à celui utilisé pour le BTC.
[[File:Color Cell Compression.jpg|centre|vignette|Color Cell Compression.]]
[[File:Dxt1-memory-layout.png|vignette|Dxt1 et ''color cell compression''.]]
Le format de compression de texture utilisé de base par Direct X, le DXTC, est une version amliorée de l'algorithme précédent. Il est décliné en plusieurs versions : DXTC1, DXTC2, etc. La première version du DXTC est une sorte d'amélioration du CCC : il ajoute une gestion minimale de transparence, et découpe la texture à compresser en ''tiles'' de 4 pixels de côté. La différence, c'est que la couleur finale d'un texel est un mélange des deux couleurs attribuée au bloc. Pour indiquer comment faire ce mélange, on trouve deux bits de contrôle par texel.
Si jamais la couleur 1 < couleur2, ces deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (2 * Couleur1 + Couleur2) / 3
* 11 = (Couleur1 + 2 * Couleur2) / 3
Sinon, les deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (Couleur1 + Couleur2) / 2
* 11 = Transparent
[[File:DXTC.jpg|centre|vignette|DXTC.]]
Le circuit de décompression du DXTC ressemble alors à ceci :
[[File:Circuit de décompression du DXTC.jpg|centre|vignette|upright=2.0|Circuit de décompression du DXTC.]]
===Les format DXTC 2, 3, 4 et 5 : l'ajout de la transparence===
Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3, lui-même replacé par le DXT4 et par le DXT5. Dans le DXT3, la transparence fait son apparition. Pour cela, on ajoute 64 bits par ''tile'' pour stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1.
[[File:Dxt23-memory-layout.png|centre|vignette|Dxt 2 et 3.]]
Dans le DXT4 et le DXT5, la méthode utilisée pour compresser les couleurs l'est aussi pour les valeurs de transparence. L'information de transparence est stockée par un en-tête contenant deux valeurs de transparence, le tout suivi d'une matrice qui attribue trois bits à chaque texel. En fonction de la valeur des trois bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1.
[[File:Dxt45-memory-layout.png|centre|vignette|Dxt 4 et 5.]]
===Le format de compression PVRTC===
Passons maintenant à un format de compression de texture un peu moins connu, mais pourtant omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle travaille surtout dans les cartes graphiques embarquées. Ses cartes se trouvent notamment dans l'ipad, l'iPhone, et bien d'autres smartphones actuels.
Avec le PVRTC, les textures sont encore une fois découpées en ''tiles'' de 4 texels par 4, mais la ressemblance avec le DXTC s’arrête là. Chacque ''tile'' est codée avec :
* une couleur codée sur 16 bits ;
* une couleur codée sur 15 bits ;
* 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
* et un bit de modulation, qui permet de configurer l’interprétation des bits de mélange.
Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4*4.
==Annexe : le ''normal-mapping'' hardware==
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, parlons un peu du ''normal mapping'' et de son implémentation dans les unités de texture. Pour rappel, 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. Et pour comprendre quel est le rapport avec les textures, nous allons devoir faire quelques rappels sur l'éclairage par pixel.
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
L'éclairage se fait en utilisant de nombreux calculs, qu'on a détaillé dans le chapitre sur les bases du rendu 3D. Ceux-ci utilisent la normale d'un sommet, à savoir un vecteur orienté à la verticale de la surface. En théorie, il y a une normale par sommet, ce qui fait que les calculs d'éclairage doivent se faire au niveau géométrique, avant la rastérisation. Mais pour obtenir un éclairage de meilleure qualité, il y a des techniques qui calcule l'éclairage d'une scène 3D pixel par pixel.
Une première technique d'éclairage par pixel interpole les normales lors de l'étape de rastérisation. On obtient alors un éclairage de Phong. Une autre solution est celle du ''normal mapping''. Le '''''normal mapping''''' précalcule les normales d'une surface dans une texture, appelée la ''normal map''. L'éclairage est alors réalisé par un pixel shader, qui lit les normales depuis cette texture et fait les calculs d'éclairage avec.
Notons que l'accès à la ''normal map'' se fait comme pour n'importe quelle texture : les texels sont lus en mémoire vidéo, puis le filtrage de texture est appliqué, et enfin le tout est envoyé au ''pixel shader''. En théorie, le filtrage effectue une sorte d'interpolation des normales automatique, ce qui permet de trouver la normale à un pixel précis, même s'il n'est pas sur un texel.
La ''normal map'' est une texture, donc. Mais ce n'est pas une texture comme une autre. Elle mémorise un vecteur pour chaque texel, pas une couleur. Il est possible d'utiliser les formats de textures vus plus haut, même si cela ne donne pas de bons résultats. Après tout, un vecteur est codé par trois coordonnées x,y,z, il est possible de coder chacune avec un octet et de packager cela dans une texture RGB classique. La coordonnée x va dans la composante Rouge, la coordonnée Y dans la composante Bleu, et la coordonnée z dans la composante z.
Le problème est que le rendu final n'est pas très beau. Et les opérations de filtrage ne donnent pas de très bons résultats. Pour remédier à ces problèmes, des formats de texture spécialisés pour les ''normal map'' ont été inventés. Le premier d'entre eux est le '''3Dc''', aussi appelé BC5 pour ''Block Compression 5''. Il utilise un octet par texel, au lieu de trois avec une texture RGB normale non-compressée.
Avec le 3Dc, les trois coordonnées des vecteurs ne sont pas mémorisées, seules deux d'entre elles le sont. En effet, les normales sont des vecteurs ''normalisés'', c'est à dire que leur longueur vaut 1, par construction. Vu que la taille est connue à l'avance, on peut en déduire la coordonnée z à partir des coordonnées x et y, avec l'usage du théorème de Pythagore. Par définition, <math>z^2 = 1 - x^2 - y^2</math>. Le calcul peut être fait dans le ''pixel shader'' sans problème.
Pour le reste, la compression de la ''normal map'' ressemble un peu à celle des textures. La ''normal map'' est découpée en tiles de 4 par 4 texels de côté, chacune étant encodée en tenant compte de certaines redondances.
==Annexe : les ''shadowmap'' hardware==
Les anciens GPU, notamment la Geforce FX, avaient des fonctionnalités spécifiques pour le calcul des ombres. Dans la plupart des jeux vidéos de l'époque, et même de maintenant, les ombres sont calculées avec la technique des ''shadowmap''. L'idée est assez simple sur le principe : un pixel est dans l'ombre quand il est invisible depuis une source de lumière.
L'idée est que le rendu est réalisé en plusieurs passes, avec une passe par source de lumière et une passe finale pour calculer l'image finale. Nous allons expliquer la technique avec une seule source de lumière, et allons utiliser l'exemple de la scène ci-dessous.
[[File:7fin.png|centre|vignette|Scène 3D d'exemple.]]
===La technique du ''shadowmapping''===
[[File:2shadowmap.png|vignette|Résultat de la première passe : ''shadowmap''..]]
La première passe rend l'image depuis le point de vue de la source de lumière. Cette première passe ne rend pas les couleurs de la scène, elle ne s'intéresse qu'à la profondeur des pixels. Le résultat est que l'image ne rend que le tampon de profondeur. Celui-ci est ensuite réutilisé comme texture pour la passe suivante. La texture en question est appelée la '''''shadownmap'''''.
La perspective utilisée, ainsi que le ''view frustrum'', dépend de la source de lumière. Pour une source de lumière qui émet un cône de lumière, le ''view frustrum'' de l'image rendue doit contenir tout le cone de lumière, et doit coller le plus possible à celui-ci. Pour une source directionnelle, comme le soleil, une perspective orthographique est utilisée.
La seconde passe rend l'image du point de vue de la caméra, pour rendre l'image finale. Elle rend l'image finale, qui est composée de pixels, chacun ayant une position à l'écran x,y, et une profondeur z. Les coordonnées sont transformées pour obtenir la position de ce pixel depuis le point de vue de la caméra. Une simple multiplication de matrice suffit, rien de bien compliqué, un shader peut le faire.
[[File:5failed.png|vignette|Résultat du test des comparaisons.]]
Après cette étape, on a alors les coordonnées x,y,z de ce pixel du point de vue de la caméra, et la ''shadowmap''. Il est alors possible d'accéder à la ''shadowmap'' au même endroit, à la même place que le pixel testé, aux mêmes coordonnées x,y. Si la profondeur du pixel est supérieure à celle de la shadowmap au même endroit, alors le pixel est situé derrière la surface visible, donc est dans l'ombre. Sinon, il n'est pas dans l'ombre. Le même procédé est répété sur chaque pixel de l'écran.
===Les optimisations hardware du ''shadowmapping''===
La technique des ''shadowmap'' demande donc de calculer une texture ''shadowmap'', puis de lire celle-ci et de faire des comparaisons de profondeur. Les GPU comme la Geforce FX intégraient du matériel dans les unités de texture pour faciliter ce travail. Les unités de texture pouvaient lire les ''shadowmap'', et faire la comparaison de profondeur toutes seules, elles avaient des circuits pour. Il suffisait de leur fournir le pixel à tester, ses coordonnées x,y,z, et l'adresse de la ''shadowmap''. Les unités de texture renvoyaient alors un résultat valant 0 ou 1 : 1 si le pixel est dans l'ombre, 0 sinon.
Elles pouvaient même effectuer du filtrage de texture sur les ''shadowmap''. Mais le filtrage était différent de celui utilisé sur les autres textures : moyenner des valeurs de profondeur ne marche pas bien. Elles utilisaient des techniques de filtrage différentes : elles faisaient les tests de comparaison, puis faisaient la moyenne des résultats. Ainsi, pour du filtrage bilinéaire, elles lisaient 4 texels dans la ''shadowmap'', puis faisaient 4 tests de comparaison, et moyennaient les 4 résultats.
==Annexe : les textures virtuelles==
Les '''textures virtuelles''' sont une optimisation des textures normales, qui visent à accélérer le rendu de terrains de grande taille. Imaginez par exemple un monde assez ouvert, comme un environnement en forêt ou en montagne, avec une grande distance de visibilité. Avec de tels terrains, le "sol" est recouvert par une texture de sol unique qui recouvre tout le terrain. Elle ne se répète pas, est de très grande taille, et peut parfois recouvrir toute la map ! Mais il n'y a pas assez de mémoire vidéo pour mémoriser la texture toute entière. La seule solution est la suivante : une partie de la texture est placée en mémoire vidéo, le reste est soit placé en mémoire RAM ou sur le disque dur.
Pour cela, le moteur de jeu utilise une optimisation ingénieuse, basée sur une observation assez basique : une bonne partie de la texture est visible, mais le reste est caché par des arbres, des habitations ou d'autres obstacles. Une optimisation possible de ne garder en mémoire vidéo que les portions visibles de la texture, pas les portions cachées. Une autre optimisation mélange textures virtuelles et ''mip-mapping''. L'idée est que pour les portions lointaines d'une texture, la texture utilisée est une ''mip-map'' de basse résolution. L'idée est alors de ne charger que la ''mip-map'' adéquate, pas les autres niveaux de détail. En clair, la texture de base n'est pas chargée en mémoire vidéo, mais la ''mip-map'' basse résolution l'est.
===Une texture à deux niveaux===
L'implémentation des textures virtuelles découpe les méga-textures en ''tiles'', en morceaux rectangulaires de taille modeste. En clair, le terrain est découpé en morceau rectangulaires/carrés. Seules les tiles nécessaires sont chargées en mémoire vidéo, pas les autres. Par exemple, les ''tiles'' non-visibles ne sont pas placées en mémoire vidéo, seules les ''tiles'' visibles le sont. De même, il y a une ''tile'' par niveau de mip-map : seul la tile correspondant le niveau adéquat est en mémoire vidéo, les autres niveaux de détail ne sont pas chargés. On peut faire une analogie avec la mémoire virtuelle, où les données sont découpées en pages, qui sont chargées en mémoire RAM à la demande, suivant les besoins, les données pouvant être swappées sur le disque dur si elles sont peu utilisées. Sauf qu'ici, il s'agit de textures qui sont découpées en pages chargées à la demande en mémoire vidéo, depuis la RAM système.
Une texture virtuelle est en réalité un système à deux niveaux : une liste de ''tiles'' et les ''tiles'' elles-mêmes. La liste de ''tiles'' est appelée un '''atlas de texture''', c'est un peu l'équivalent de la ''tilemap'' pour le rendu 2D. Rendre une texture demande de calculer quelle ''tile'' contient le texel à afficher, consulter la ''tile'' en question, puis récupérer le texel adéquat dans cette ''tile''. La ''tile'' est donc une texture, mais la texture à charger est choisie parmi un ensemble, qui est ici l'atlas de texture.
===L'implémentation : logicielle versus matérielle===
Les textures virtuelles ont été utilisées pour la première fois par les jeux Rage 1 et 2 d'IdSoftware, et quelques jeux ultérieurs comme DOOM 2016. IdSoftware les appelait des '''''mega-textures'''''. L'optimisation permettait des gains en performance assez impressionnants. Le jeu Rage 1 utilisait une texture carrée unique de 128k pixels de côté pour rendre le terrain. En théorie, une telle texture devrait prendre 64 giga-octets, mais le jeu tournait correctement avec 512 méga-octets de RAM, poussivement avec seulement 256 méga-octets de RAM.
De nos jours, les textures virtuelles sont supportées par beaucoup de jeux vidéos, les moteurs les plus courants gèrent de telles textures de manière logicielles. Mais quelques GPU récents supportent les textures virtuelles. Sur les GPU récents, l'atlas de texture est géré nativement par le matériel. Le GPU choisit quelle ''tile'', quelle texture choisir pour rendre le texel adéquat. Pour cela, le GPU calcule quelle ''tile'' charger, consulte l'atlas de texture, et lit la texture de ''tile'' adéquate.
Mais l'implémentation sur les GPU récents a de nombreuses limitations. La limitation la plus importante est que la taille des textures virtuelles ne peut pas dépasser la taille d'une texture normale, soit 32768 pixels de côté pour une texture carrée environ sur les GPU de 2020. De plus, le chargement d'une ''tile'' est très lent. En clair, dès qu'on veut changer de niveau de mip-map pour une tile, ou dès qu'une tile devient visible, le chargement de la tile peut facilement prendre plusieurs centaines de millisecondes. Le filtrage de texture est très complexe avec des textures virtuelles, ce qui fait que le filtrage de texture virtuelle est souvent soumis à des limitations que les textures normales n'ont pas, notamment pour le filtrage anisotropique.
{{NavChapitre | book=Les cartes graphiques
| prev=Le rasterizeur
| prevText=Le rasterizeur
| next=Les Render Output Target
| nextText=Les Render Output Target
}}{{autocat}}
plfrv504a0261r9xoy6rfehyd5x66f5
763407
763406
2026-04-10T17:05:03Z
Mewtow
31375
/* Annexe : le normal-mapping hardware */
763407
wikitext
text/x-wiki
[[File:Texture mapping.png|vignette|''Texture mapping'']]
Les '''textures''' sont des images que l'on va plaquer sur la surface d'un objet, du papier peint en quelque sorte. Les cartes graphiques supportent divers formats de textures, qui indiquent comment les pixels de l'image sont stockés en mémoire : RGB, RGBA, niveaux de gris, etc. Une texture est donc composée de "pixels", comme toute image numérique. Pour bien faire la différence entre les pixels d'une texture, et les pixels de l'écran, les pixels d'une texture sont couramment appelés des ''texels''.
==Le placage de textures inverse==
Pour rappel, plaquer une texture sur un objet consiste à attribuer un texel à chaque sommet, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture.
Dans les faits, on n'utilise pas de coordonnées entières de ce type. Les coordonnées de texture sont deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. Le nom donnée à cette technique de description des coordonnées de texture s'appelle l''''''UV Mapping'''''.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Les API 3D modernes gèrent des textures en trois dimensions, ce qui ajoute une troisième coordonnée de texture notée w. Dans ce qui va suivre, nous allons passer les textures en trois dimensions sous silence. Elles ne sont pas très utilisées, la quasi-totalité des jeux vidéo et applications 3D utilisant des textures en deux dimensions. Par contre, le matériel doit gérer les textures 3D, ce qui le rend plus complexe que prévu. Il faut ajouter quelques circuits pour, de quoi gérer la troisième coordonnée de texture, etc.
Lors de la rastérisation, chaque fragment se voit attribuer un sommet, et donc la coordonnée de texture qui va avec. Si un pixel est situé pile sur un sommet, la coordonnée de texture de ce sommet est attribuée au pixel. Si ce n'est pas le cas, la coordonnée de texture finale est interpolée à partir des coordonnées des trois sommets du triangle rastérisé. L'interpolation en question a lieu dans l'étape de rastérisation, comme nous l'avons vu dans le chapitre précédent. Le fait qu'il y ait une interpolation fait que les coordonnées du pixel gagent à être des nombres flottants. On pourrait faire une interpolation avec des coordonnées de texture entières, mais les arrondis et autres imprécisions de calcul donneraient un résultat graphiquement pas terrible, et empêcheraient d'utiliser les techniques de filtrage de texture que nous verrons dans ce chapitre.
À partir de ces coordonnées de texture, la carte graphique calcule l'adresse du texel qui correspond, et se charge de le lire. Et toute la magie a lieu dans ce calcul d'adresse, qui part de coordonnées de texture flottante, pour arriver à une adresse mémoire. Le calcul de l'adresse du texel se fait en plusieurs étapes, que nous allons voir ci-dessous. La première étape convertit les coordonnées flottantes en coordonnées entières, qui disent à quel ligne et colonne se trouve le texel voulu dans la texture. L'étape suivante transforme ces coordonnées x,y entières en adresse mémoire.
===La normalisation des coordonnées===
J'ai dit plus haut que les coordonnées de texture sont des coordonnées flottantes, comprises entre 0 et 1. Mais il faut savoir que les pixels shaders peuvent modifier celles-ci pour mettre en œuvre certains effets graphiques. Et le résultat peut alors se retrouver en-dehors de l'intervalle 0,1. C'est quelque chose de voulu et qui est traité par la carte graphique automatiquement, sans que ce soit une erreur. Au contraire, la manière dont la carte graphique traite cette situation permet d'implémenter des effets graphiques comme des textures en damier ou en miroir.
[[File:Clamp tile.jpg|vignette|Clamp tile]]
Il existe globalement trois méthodes très simples pour gérer cette situation, qui sont appelés des '''modes d'adressage de texture'''.
* La première méthode est de faire en sorte que le résultat sature. Si une coordonnée est inférieur à 0, alors on la remplace par un zéro. Si elle est supérieure à 1, on la ramène à 1. Avec cette méthode, tout se passe comme si les bords de la texture étaient étendus et remplissaient tout l'espace autour de la texture. Le tout est illustré ci-dessous. Ce mode d'accès aux textures est appelé le '''''clamp'''''.
* Une autre solution retire la partie entière de la coordonnée, elle coupe tout ce qui dépasse 1. Pour le dire autrement, elle calcule le résultat modulo 1 de la coordonnée. Le résultat est que tout se passe comme si la texture était répétée à l'infini et qu'elle pavait le plan.
* Une autre méthode remplit les coordonnées qui sortent de l’intervalle 0,1 avec une couleur préétablie, configurée par le programmeur.
===La conversion des coordonnées de textures flottantes en adresse mémoire===
Une fois la normalisation effectuée, les coordonnées de texture sont utilisées pour lire le texel voulu. Pour cela, les coordonnées de texte sont transformées en adresse mémoire, adresse qui pointe sur le texel ayant ces cordonnées. Pour cela, la première étape est de transformer les coordonnées flottantes u,v en coordonnées entières x,y qui pointent sur un texel. Pour cela, il suffit de multiplier les coordonnées flottantes u,v par la résolution de la texture accédée. Pour un écran de résolution <math>\text{height,width}</math>, le calcul est le suivant :
: <math>x = u \times \text{width}</math>
: <math>y = v \times \text{height}</math>
Le résultat est un nombre avec une partie entière et une partie fractionnaire. La partie entière des deux coordonnées donne la position x,y voulue, et la partie fractionnaire est conservée pour le filtrage de textures, mais passons cela sous silence pour le moment.
La seconde étape prend les coordonnées entières x,y et calcule l'adresse mémoire du texel. L'adresse dépend de la position de la texture en mémoire, précisément de son début, son premier texel, mais aussi de la position du texel par rapport au début de la texture. Et calculer cette position intra-texture dépend de la manière dont les texels sont stockés en mémoire.
====Les textures naïves====
Les programmeurs qui lisent ce cours s'attendent certainement à ce que la texture soit stockée en mémoire ligne par ligne, ou colonne par colonne. Cela veut dire que le premier pixel en partant d'en haut à gauche est stocké en premier, puis celui immédiatement à sa droite, puis celui encore à droite, et ainsi de suite. Une fois qu'on arrive à la fin d'une ligne, on passe à la ligne suivante, en-dessous. Cette organisation ligne par ligne s'appele l'organisation '''''row major order'''''. On peut faire pareil, mais colonne par colonne, ce qui donne le '''''column major order'''''.
[[File:Speicheranordnung Feld.svg|centre|vignette|upright=2|Row et column major order.]]
Maintenant, supposons que la texture commence à l'adresse <math>A_\text{texture}</math>, qui est l'adresse du premier texel. La texture a une résolution de <math>\text{width}</math> texels de large et <math>\text{height}</math> texels de haut. Par définition, les coordonnées X et Y des texels commencent à 0, ce qui fait que le pixel en haut à gauche a les coordonnées 0,0.
L'adresse du pixel se calcule comme suit :
: <math>A_\text{pixel} = A_\text{texture} + (\text{taille d'une ligne en octets} \times Y) + (\text{taille d'un texel en octets} \times X)</math>
La taille d'un pixel en mémoire est notée T. La taille d'une ligne en mémoire est de <math>width \times T</math>, par définition, vu qu'elle fait <math>width</math> texels. On a donc :
: <math>A_\text{pixel} = A_\text{texture} + (width \times T \times Y) + (T \times X)</math>
La formule se réécrit comme suit :
: <math>A_\text{pixel} = A_\text{texture} + T \times (width \times Y + X)</math>
Le calcul d'adresse est donc assez simple. Malheureusement, les textures ne sont pas stockées de cette manière en mémoire vidéo. En effet, elle se marie mal avec les opérations de filtrage de texture que nous allons voir dans ce qui suit. Le filtrage d'un texel dépend de ses voisins du dessus et du dessous. Le fait que la texture n'est pas forcément parcourue ligne par ligne fait que stocker une texture ligne par ligne n'est pas l'idéal.
De même, les textures sont déformées par la perspective. L'affichage de la texture ne se fait alors pas ligne par ligne, mais en parcourant la texture en diagonale, l'angle de la diagonale correspondant approximativement à l'angle que fait la verticale de la texture avec le regard. Vu qu'on ne connait pas à l'avance l'angle que fera la diagonale de parcours, on doit ruser.
====Les textures tilées====
Une première solution à ce problème est celle des '''textures tilées'''. Avec ces textures, l'image de la texture est découpée en ''tiles'', des rectangles ou en carrés de taille fixe, généralement des carrés de 4 pixels de côté. Les tiles ont une largeur et une longueur égales, afin de simplifier les calculs : on divise X et Y par le même nombre. De plus, leur largeur et leur longueur sont une puissance de deux, afin de simplifier les calculs d'adresse. Les ''tiles'' sont alors mémorisée les unes après les autres dans le fichier de la texture.
[[File:Texture tilée.png|centre|vignette|upright=2|Texture tilée]]
La formule de calcul d'adresse vue plus haut doit être adaptée pour tenir compte des tiles. Pour cela, il faut remplacer la taille d'un texel par la taille d'une tile, et que la largeur de la texture soit exprimée en nombre de tiles. De plus, on doit adapter les coordonnées des texels pour donner des coordonnées de tile. Généralement, les tiles sont des carrés de N pixels de côté, ce qui fait qu'on peut regrouper les lignes et les colonnes par paquets de N. Il suffit donc de diviser Y et X pour obtenir les coordonnées de la tile, de même que la larguer. La formule pour calculer la position de la énième tile est alors la suivante :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + \text{Taille mémoire d'une tile} \times \left( {\text{Width} \over N} \times {Y \over N} + {X \over N} \right)</math>
On peut réécrire le tout comme suit :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + K \times \left( {Y \over N} + X \right)</math>, avec K une constante connue à la compilation des shaders.
Vu que les tiles sont carrées avec une largeur qui est une puissance de deux, la multiplication par la taille d'une tile en mémoire se simplifie : on passe d'une multiplication entière à des décalages de bits. Même chose pour le calcul de l'adresse de la tile à partir des coordonnées x,y : ils impliquent des divisions par une puissance de deux, qui deviennent de simples décalages.
La position d'un pixel dans une tile dépend du format de la texture, mais peut se calculer avec quelques calculs arithmétiques simples. Dans les cas les plus simples, les pixels sont mémorisés ligne par ligne, ou colonne par colonne. Mais ce n'est pas systématiquement le cas. Toujours est-il que les calculs pour déterminer l'adresse sont simples, et ne demandent que quelques additions ou multiplications. Mais avec les formats de texture utilisés actuellement, les tiles sont chargées en entier dans le cache de texture, sans compter que diverses techniques de compression viennent mettre le bazar, comme on le verra dans la suite de cours.
Un avantage de l'organisation en tiles est qu'elle se marie bien avec le parcours des textures. On peut parcourir une texture dans tous les sens, horizontal, vertical, ou diagonal, on sait que les prochains pixels ont de fortes chances d'être dans la même tile. Si on rentre dans une tile par la gauche en haut, on a encore quelques pixels à parcourir dans la tile, par exemple. De même, le filtrage de textures est facilité. On verra dans ce qui va suivre que le filtrage de texture a besoin de lire des blocs de 4 texels, des carrés de 2 pixels de côté. Avec l'organisation en tile, on est certain que les 4 texels seront dans la même tile, sauf s'ils ont le malheur d'être tout au bord d'une tile. Ce dernier cas est assez rare, et il l'est d'autant plus que les tiles sont grandes. Enfin, un dernier avantage est que les tiles sont généralement assez petites pour tenir tout entier dans une ligne de cache. Le cache de texture est donc utilisé à merveille, ce qui rend les accès aux textures plus rapides.
====Les textures basées sur des ''z-order curves''====
Les formats de textures théoriquement optimaux utilisent une '''''Z-order curve''''', illustrée ci-dessous. L'idée est de découper la texture en quatre rectangles identiques, et de stocker ceux-ci les uns à la suite des autres. L'intérieur de ces rectangles est lui aussi découpé en quatre rectangles, et ainsi de suite. Au final, l'ordre des pixels en mémoire est celui illustré ci-dessous.
[[File:Z-CURVE.svg|centre|vignette|upright=2|Construction d'une ''Z-order curve''.]]
Les texels sont stockés les uns à la suite des autres dans la mémoire, en suivant l'ordre donnée par la ''Z-order curve''. Le calcul d'adresse calcule la position du texel en mémoire, par rapport au début de la texture, et ajoute l'adresse du début de la texture. Mais tout le défi est de calculer la position d'un texel en mémoire, à partir des coordonnées x,y. Le calcul peut sembler très compliqué, mais il n'en est rien. Le calcul demande juste de regarder les bits des deux coordonnées et de les combiner d'une manière particulièrement simple. Il suffit de placer le bit de poids fort de la coordonnée x, suivi de celui de la coordonnée y, et de faire ainsi de suite en passant aux bits suivants.
[[File:Zcurve45bits.png|centre|vignette|upright=1.5|Calcul de la position d'un élément dans une ''Z-order curve'' à partir des coordonnées x et y.]]
L'avantage d'une telle organisation est que la textures est découpées en ''tiles'' rectangulaires d'une certaine taille, elles-mêmes découpées en ''tiles'' plus petites, etc. Et il se trouve que cette organisation est parfaite pour le cache de texture. L'idéal pour le cache de texture est de charger une ''tile'' complète dans le cache de textures. Quand on accède à un texel, on s'assure que la ''tile'' complète soit chargée. Mais cela demande de connaitre à l'avance la taille d'une ''tile''. Les formats de texture fournissent généralement une ''tile'' carré de 4 pixels de côté, mais cela donnerait un cache trop petit pour être vraiment utile. Avec cette méthode, on s'assure qu'il y ait une ''tile'' avec la taille optimale. Les ''tiles'' étant découpées en ''tiles'' plus petites, elles-mêmes découpées, et ainsi de suite, on s'assure que la texture est découpées en ''tiles'' de taille variées. Il y aura au moins une ''tile'' qui rentrera tout pile dans le cache.
==Les techniques de rendu à textures multiples==
Nous venons de voir comment une texture est plaquée sur un objet 3D, ou une surface comme un sol. Pour résumer, le calcul de l'adresse d'un texel prend la position du texel par rapport au début de la texture, et ajoute l'adresse du début de la texture. L'adresse mémoire de la texture est connue au moment où le pilote de la carte graphique place la texture dans la mémoire vidéo, et cette information est transmise au matériel par l'intermédiaire du processeur de commande, puis passée aux processeurs de shaders et à l'unité de texture. Le tout est couplé à d'autres informations, la plus importante étant la ''taille de la texture en octets'', pour éviter de déborder lors des accès à la texture.
Néanmoins, il s'agit là du cas le plus simple. Certaines techniques de rendu demandent de choisir la texture à plaquer parmi un ensemble de plusieurs textures. Les techniques en question sont assez variées et n'ont pas grand chose en commun. Les plus connues sont le ''mip-mapping'', le ''cube-mapping'' et les textures virtuelles. Le ''mip-mapping'' sert à filtrer les textures, chose qu'on expliquera plus tard, le ''cube-mapping'' sert à simuler des réflexions sur un objet en plaquant une texture de l'environnement dessus, les textures virtuelles sont une optimisation pour les textures des terrains de grande taille. Mais malgré leurs différences, elles demandent de choisir quelle texture plaquer entre plusieurs textures de base. En clair, l'adresse de base de la texture varie selon la situation. Voyons-les dans le détail.
===Le mip-mapping===
Le '''mip-mapping''' a pour but de légèrement améliorer les graphismes des objets lointains, tout en rendant les calculs de texture plus rapides. Formellement, le ''mip-mapping'' est une technique de filtrage de texture, mais nous l'abordons maintenant car elle est surtout liée au calcul d'adresse. Les unités de texture ont des circuits de filtrage de texture séparés des circuits de ''mip-mapping'' et de calcul d'adresse, d'où le fait que nous en parlons séparément.
Le problème résolu par le ''mip-mapping'' est le rendu des textures lointaines. Si une texture est plaquée sur un objet lointain, une bonne partie des détails est invisible pour l'utilisateur. Un pixel de l'écran est associé à plusieurs texels. Idéalement, la carte graphiques devrait lire tous ces texels et en faire une sorte de moyenne pondérée, pour calculer la couleur finale du pixel. Mais dans les faits, ce serait très gourmand et compliqué à implémenter en hardware. Une solution serait de ne garder que quelque texels, mais cela a tendance à créer des artefacts visuels (les textures affichées ont tendance à pixeliser). Le ''mip-mapping'' permet de réduire ces deux problèmes en même temps en précalculant cette moyenne pondérée pour des distances prédéfinies.
L'idée est d'utiliser plusieurs exemplaires d'une même texture à des résolutions différentes, chaque exemplaire étant adapté à une certaine distance. Par exemple, une texture sera stocké avec un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre de 128 * 128 et ainsi de suite jusqu’à un dernier exemplaire de 32 * 32 pixel. Chaque exemplaire correspond à un '''niveau de détail''', aussi appelé ''Level Of Detail'' (abrévié en LOD). La résolution utilisée diminue d'autant plus que l'objet est situé loin de la caméra. Les objets proches seront rendus avec la texture 512*512, ceux plus lointains seront rendus avec la texture de résolution 256*256, les textures 128*128 seront utilisées encore plus loin, et ainsi de suite jusqu'aux objets les plus lointains qui sont rendus avec la texture la plus petite de 32*32.
[[File:MipMap Example STS101.jpg|centre|vignette|upright=2|Exemples de mip-maps.]]
Le ''mip-mapping'' améliore grandement la qualité d'image. L'image d'exemple ci-dessous le montre assez bien.
[[File:Mipmapping example.png|centre|vignette|upright=2|Exemple de mipmapping.]]
Pour faciliter les calculs d'adresse, les LOD d'une même texture sont stockées les uns après les autres en mémoire (dans un tableau, comme diraient les programmeurs). Ainsi, pas besoin de se souvenir de la position en mémoire de chaque LOD : l'adresse de la texture de base, et quelques astuces arithmétiques suffisent. Prenons le cas où la texture de base a une taille L. le premier exemplaire est à l'adresse 0, le second niveau de détail est à l'adresse L, le troisième à l'adresse L + L/4, le suivant à l'adresse L + L/4 + L/16, et ainsi de suite. Le calcul d'adresse demande juste connaître le niveau de détails souhaité et l'adresse de base de la texture. Le niveau de détail voulu est calculé par les pixel shaders, en fonction de la coordonnée de profondeur du pixel à traiter.
Évidemment, cette technique consomme de la mémoire vidéo, vu que chaque texture est dupliquée en plusieurs exemplaires, en plusieurs LOD. Dans le détail, la technique du mip-mapping prend au maximum 33% de mémoire en plus (sans compression). Cela vient du fait qu'en prenant une texture dexu fois plus petite, elle prend 4 fois moins de mémoire : 2 fois moins de pixels en largeur, et 2 fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des LODs, texture de base comprise, prendra X + (X/4) + (X/16) + (X/256) + … Un petit calcul de limite donne 4/3 * X, soit 33% de plus.
===Le cube-mapping===
[[File:Cube mapped reflection example 2.JPG|vignette|Exemple de reflets environnementaux.]]
L''''environnement-mapping''' est une technique de calcul de divers effets graphiques liés à l'environnement, notamment des réflexions. L'idée est de plaquer une texture pré-calculée pour simuler l'effet de l'environnement sur une surface ou un objet 3D. Il en existe plusieurs versions différentes, mais la seule utilisée de nos jours est le ''cube-mapping'', où la texture de l'environnement est plaquée sur un cube, d'où son nom. Le cube en question est utilisé différemment suivant ce que l'on cherche à faire avec le ''cube-mapping''. Les deux utilisations principales sont le rendu du ciel et des décors, et les réflexions sur la surface des objets. Dans les deux cas, l'idée est de précalculer ce que l'on voit du point de vue de la caméra. On place la caméra dans la scène 3D, on place un cube centré sur la caméra, le cube est texturé avec ce que l'on voit de l'environnement depuis la caméra/l'objet de son point de vue.
[[File:Panorama cube map.png|centre|vignette|upright=2|L'illustration montre en premier lieu une ''cubemap'' avec les six faces mises en évidence, puis quel environnement 3D elle permet de simuler, le troisième illustration montrant comment la ''cubemap'' est utilisée pour simuler l'environnement.]]
Le rendu du ciel et des décors lointains dans les jeux vidéo se base sur des '''''skybox''''', à savoir un cube centré sur la caméra, sur lequel on ajoute des textures de ciel ou de décors lointains. Le cube est recouvert par une texture, qui correspond à ce que l'on voit quand on dirige le regard de la caméra vers cette face. Contrairement à ce qu'on pourrait croire, la skybox n'est pas les limites de la scène 3D, les limites du niveau d'un jeu vidéo ou quoique ce soit d'autre de lié à la physique de la scène 3D. La skybox est centrée sur la caméra, elle suit la caméra dans son mouvement. Centrer la skybox sur la caméra permet de simuler des décors très lointains, suffisamment lointain pour qu'on n'ait pas l'illusion de s'en rapprocher en se déplaçant dans la map. De plus, cela évite d'avoir à faire trop de calculs à chaque fois que l'on bouge la caméra. La texture plaquée sur le cube est une texture unique, elle-même découpée en six sous-textures, une par face du cube.
[[File:Skybox example.png|centre|vignette|upright=2|Exemple de Skybox.]]
[[File:Cube mapped reflection example.jpg|vignette|Réflexions calculées par une ''cubemap''.]]
Le ''cube-mapping'' est aussi utilisé pour des reflets. L'idée est de simuler les reflets en plaquant une texture pré-calculée sur l'objet réflecteur. La texture pré-calculée est un dessin de l'environnement qui se reflète sur l'objet, un dessin du reflet à afficher. En la plaquant la texture sur l'objet, on simule ainsi des reflets de l'environnement, mais on ne peut pas calculer d'autres reflets comme les reflets objets mobiles comme les personnages. Et il se trouve que la texture pré-calculée est une ''cubemap''. Pour les environnements ouverts, c'est la ''skybox'' qui est utilisée, ce qui permet de simuler les reflets dans les flaques d'eau ou dans des lacs/océans/autres. Pour les environnements intérieurs, c'est une cubemap spécifique qui utilisée. Par exemple, pour l'intérieur d'une maison, on a une ''cubemap'' par pièce de la maison. Les reflets se calculent en précisant quelle ''cubemap'' appliquer sur l'objet en fonction de la direction du regard.
[[File:Cube map level.png|centre|vignette|Cube map de l'intérieur d'une pièce d'un niveau de jeux vidéo.]]
Toujours est-il que les textures utilisées pour le ''cubemmapping'', appelées des ''cubemaps'', sont en réalité la concaténation de six textures différentes. En mémoire vidéo, la ''cubemap'' est stockée comme six textures les unes à la suite des autres. Lors du rendu, on doit préciser quelle face du cube utiliser, ce qui fait 6 possibilités. On a le même problème qu'avec les niveaux de détail, sauf que ce sont les faces d'une ''cubemap'' qui remplacent les textures de niveaux de détails. L'accès en mémoire doit donc préciser quelle portion de la ''cubemap'' il faut accéder. Et l'accès mémoire se complexifie donc. Surtout que l'accès en question varie beaucoup suivant l'API graphique utilisée, et donc suivant la carte graphique.
Les API 3D assez anciennes ne gérent pas nativement les ''cubemaps'', qui doivent être émulées en logiciel en utilisant six textures différentes. Le pixel shader décide donc quelle ''cubemap'' utiliser, avec quelques calculs sur la direction du regard. L'accès se fait d'une manière assez simple : le shader choisit quelle texture utiliser. Les API 3D récentes gèrent nativement les ''cubemaps''. Dans le cas le plus simple,pour les versions les plus vielles de ces API, les six faces sont numérotées et l'accès à une ''cubemap'' précise quel face utiliser en donnant son numéro. La carte graphique choisit alors automatiquement la bonne texture, mais cela demande de laisser le calcul de la bonne face au pixel shader. D'autres API 3D et cartes graphiques font autrement. Dans les API 3D modenres, les ''cubemap'' sont gérées comme des textures en trois dimensions, adressées avec trois coordonnées u,v,w. La carte graphique utilise ces trois coordonnées de manière à en déduire quelle est la face pertinente, mais aussi les coordonnées u,v dans la texture de la face.
==L'implémentation matérielle du placage de textures==
Pour résumer, la lecture d'un texel demande d'effectuer plusieurs étapes. Dans le cas le plus simple, sans ''mip-mapping'' ou ''cubemapping'', on doit effectuer les étapes suivantes :
* Il faut d'abord normaliser les coordonnées de texture pour qu'elles tombent dans l'intervalle [0,1] en fonction du mode d'adressage désiré.
* Ensuite, les coordonnées u,v doivent être converties en coordonnées entières, ce qui demande une multiplication flottante.
* Enfin, l'adresse finale est calculée à partir des coordonnées entières et en ajoutant l'adresse de base de la texture (et éventuellement avec d'autres calculs arithmétiques suivant le format de la texture).
Tout cela pourrait être fait par le pixel shaders, mais cela implique beaucoup de calculs répétitifs et d'opérations arithmétiques assez lourdes, avec des multiplications flottantes, des additions et des multiplications entières, etc. Faire faire tous ces calculs par les shaders serait couteux en performance, sans compter que les shaders deviendraient plus gros et que cela aurait des conséquences sur le cache d'instruction. De plus, certaines de ces étapes peuvent se faire en parallèle, comme les deux premières, ce qui colle mal avec l'aspect sériel des shaders.
Aussi, les processeurs de shaders incorporent une unité de calcul d'adresse spéciale pour faire ces calculs directement en matériel. L'unité de texture contient au minimum deux circuits : un circuit de calcul d'adresse, et un circuit d'accès à la mémoire. Toute la difficulté tient dans le calcul d'adresse, plus que dans le circuit de lecture. Le calcul d'adresse est conceptuellement réalisé en deux étapes. La première étape qui transforme les coordonnées u,v en coordonnées x,y qui donne le numéro de la ligne et de la colonne du texel dans la texture. La seconde étape prend ces deux coordonnées x,y, l'adresse de la texture, et détermine l'adresse de la tile à lire.
[[File:Unité de texture simple.png|centre|vignette|upright=2|Unité de texture simple]]
===L'implémentation du mip-mapping===
Le ''mip-mapping'' est lui aussi pris en charge par l'unité de calcul d'adresse, car cette technique change l'adresse de base de la texture. La gestion du ''mip-mapping'' est cependant assez complexe. Il est possible de laisser le pixel shader calculer quel niveau de détail utiliser, en fonction de la coordonnée de profondeur z du pixel à afficher. La carte graphique détermine alors automatiquement quelle texture lire, quel niveau de détail, automatiquement. Elle détermine aussi la bonne résolution pour la texture, qui est égal à la résolution de la texture de base, divisée par le niveau de détail. Pour résumer, le niveau de détail est envoyé aux unités de texture, qui s'occupent de calculer l'adresse de base et la résolution adéquates. Quelques calculs arithmétiques simples, donc, qui s'implémentent facilement avec quelques circuits.
Mais une autre méthode laisse la carte graphique déterminer le niveau de détail par elle-même. Dans ce cas, cela demande, outre les deux coordonnées de texture, de calculer la dérivée de ces deux coordonnées dans le sens horizontal et vertical, ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Les quatre dérivées sont les suivantes :
: <math>\frac{du}{dx}</math>, <math>\frac{dv}{dx}</math>, <math>\frac{du}{dy}</math>, <math>\frac{dv}{dy}</math>
Un bon moyen pour obtenir les dérivées demande de regrouper les pixels par groupes de 4 et de faire la différence entre leurs coordonnées de texture respectives. On peut calculer les deux dérivées horizontales en comparant les deux pixels sur la même ligne, et les deux dérivées verticales en comparant les deux pixels sur la même colonne. Mais cela demande de rastériser les pixels par groupes de 4, par ''quads''. Et c'est ce qui est fait sur les cartes graphiques actuelles, qui rastérisent des groupes de 4 pixels à la fois.
[[File:Texture sampler unit with mipmapping.png|centre|vignette|upright=2.0|Unité de texture avec mipmapping.]]
Malheureusement, le calcul exact utilisé pour le choix de la mip-map dépend du GPU considéré et peu de chose est connu quant à ces algorithmes. Il est possible d'inférer le comportement à partir d'observations, mais guère plus. Pour ceux qui veulent en savoir plus, je conseille la lecture de cet article de blog :
* [https://pema.dev/2025/05/09/mipmaps-too-much-detail/ Mipmap selection in too much detail]
===La gestion des accès mémoire===
Enfin, l'unité de texture doit tenir compte du fait que la mémoire vidéo met du temps à lire une texture. En théorie, l'unité de texture ne devrait pas accepter de nouvelle demande de lecture tant que celle en cours n'est pas terminée. Mais faire ainsi demanderait de bloquer tout le pipeline, de l'''input assembler'' au unités de''shaders'', ce qui est tout sauf pratique et nuirait grandement aux performances.
Une solution alternative consiste à mettre en attente les demandes de lectures de texture pendant que la mémoire est occupée. La manière la plus simple d'implémenter des accès mémoire multiples est de les mettre en attente dans une petite mémoire FIFO. Cela implique que les accès mémoire s’exécutent dans l'ordre demandé par le ''shader'' et/ou l'unité de rastérisation, il n'y a pas de réorganisation des accès mémoire ou d’exécution dans le désordre des accès mémoire.
[[File:Texture prefetching.png|centre|vignette|upright=1.5|Accès mémoire simultanés.]]
Évidemment, quand la mémoire FIFO est pleine, le pipeline est alors totalement bloqué. Le rasteriser est prévenu que l'unité de texture ne peut pas accepter de nouvelle lecture de texture. En pratique, la FIFO est généralement d'une taille respectable et permet de mettre en attente beaucoup de demandes de lecture de texture. Il faut de plus noter qu'il y a une FIFO par processeur de ''shader'' sur les cartes graphiques modernes. Quand elle est pleine, le processeur cesse d'exécuter de nouveaux accès mémoire, mais peut continuer à exécuter des ''shaders'' dans les autres unités de calcul, pas besoin de bloquer complétement le pipeline.
===L'intégration du cache de textures===
Il faut noter que les unités de texture incorporent aussi un cache de texture, voire plusieurs. L'intégration des caches de texture avec la mémoire FIFO précédente est quelque peu compliqué, car il faut garantir que les lectures de texture se fassent dans le bon ordre. On ne peut pas exécuter une lecture dans le cache alors que des lectures précédentes sont en attente de lecture en mémoire vidéo. Et cela pose un gros problème : une lecture dans le cache de texture prend quelques dizaines de cycles d'horloge, alors qu'une lecture en mémoire vidéo en prend facilement 400 à 800 cycles, parfois plus. Et cela fait que l'ordre des accès mémoire peut s'inverser.
Prenons par exemple un accès au cache précédé et suivi par deux accès en mémoire vidéo. Le premier démarre au cycle 1, et se termine au cycle numéro 400. L'accès au cache commence au cycle 2 et se termine 20 cycles après, au cycle numéro 22. En clair, la lecture dans le cache s'est terminée avant l'accès mémoire qui le précède. Les textures ne sont donc plus lues dans l'ordre. Et il faut trouver une solution pour éviter cela.
La solution est de retarder les lectures dans le cache tant que tous les accès précédents ne sont pas terminés. Mais pour retarder les lectures en question, il faut d'abord savoir si la lecture atterrit dans le cache ou non, ce qui demande d'accéder au cache. On fait face à un dilemme : on veut retarder les accès au cache, mais les différencier des lectures déclenchant des accès mémoire demande d'accéder au cache en premier lieu. La solution est décrite dans l'article "Prefetching in a Texture Cache Architecture" par Igehy et ses collègues. Elle se base sur deux idées combinées ensemble.
La première idée est de séparer l'accès au cache en deux : une étape qui vérifie si les texels à lire sont dans le cache, et une étape qui accède aux données dans le cache lui-même. Un cache de texture est donc composé de deux circuits principaux. Le premier vérifie la présence des texels dans le cache. Il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Ensuite, en plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
La seconde idée est de retarder l'accès au cache entre les deux phases. La première étape d'un accès mémoire vérifie si la donnée est dans le cache ou non. Puis, on retarde la lecture des données, pour attendre que toutes les lectures précédentes soient terminées. Et enfin, troisième étape : la lecture des texels dans la mémoire cache proprement dite. Les accès mémoire passant par la mémoire vidéo se font de la même manière, à une différence près : la lecture dans le cache est remplacée par la lecture en mémoire vidéo. Tout démarre avec une demande à l'unité de tags, qui vérifie si le texel est dans le cache ou non. Puis on retarde l'accès tant que la mémoire vidéo est occupée, puis on effectue la lecture en mémoire vidéo.
Si ce n'est pas le cas, l'accès mémoire est envoyé à la mémoire vidéo comme précédemment, à savoir qu'il est mis en attente dans une mémoire FIFO, puis envoyé à la mémoire vidéo dès que celle-ci est libre. Mais en sortie de la mémoire, la donnée lue est envoyée dans le cache de texture, par dans l'unité de filtrage. Pour savoir où placer la donnée lue, l'unité de tag a réservé une ligne de cache précise, une adresse bien précise. L'adresse en question est disponible en lisant une autre mémoire FIFO, qui a mis en attente l'adresse en question, en attendant que l'accès mémoire se termine. La donnée est alors écrite dans le cache, puis lue par l'unité de filtrage de textures.
Pour une lecture dans le cache, le déroulement est similaire, mais sans le passage par la mémoire. La lecture fait une demande à l'unité de tag, et celle-ci répond que la donnée est bien dans le cache. Elle place alors l'adresse à lire dans la file d'attente. Une fois que les accès mémoire précédents sont terminés, l'adresse sort de la file d'attente et est envoyée à la mémoire de données. La lecture s'effectue, les texels sont envoyés à l'unité de filtrage de textures. La seule différence avec un ''phased cache'' normal est l'insertion de l'adresse à lire dans une FIFO qui vise à mettre en attente
[[File:Unité de texture avec un cache de texture.png|centre|vignette|upright=2.0|Unité de texture avec un cache de texture]]
Pour résumer, l'implémentation précédente garantit une exécution des lectures dans leur ordre d'arrivée. Et pour cela, elle retarde les lectures dans le cache tant que les lectures en mémoire précédentes ne sont pas terminées. L'accès au cache est plus rapide que l'accès en mémoire vidéo, mais le retard ajouté pour garantir l'ordre des lectures fait que le temps d'accès est très long.
==Le filtrage de textures==
Plaquer des textures sans autre forme de procès ne suffit pas à garantir des graphismes d'une qualité époustouflante. La raison est que les sommets et les texels ne tombent pas tout pile sur un pixel de l'écran : le sommet associé au texel peut être un petit peu trop en haut, ou trop à gauche, etc. Une explication plus concrète fait intervenir les coordonnées de texture. Souvenez-vous que lorsque l'on traduit une coordonnée de texture u,v en coordonnées x,y, on obtient un résultat qui ne tombe pas forcément juste. Souvent, le résultat a une partie fractionnaire. Si celle-ci est non-nulle, cela signifie que le texel/sommet n'est pas situé exactement sur le pixel voulu et que celui-ci est situé à une certaine distance. Concrètement, le pixel tombe entre quatre texels, comme indiqué ci-dessous.
[[File:Filtrage texture.png|centre|vignette|upright=2.0|Position du pixel par rapport aux texels.]]
Pour résoudre ce problème, on doit utiliser différentes techniques d'interpolation, aussi appelées techniques de '''filtrage de texture''', qui visent à calculer la couleur du pixel final en fonction des texels qui l'entourent. Il existe de nombreux types de filtrage de textures, qu'il s'agisse du filtrage linéaire, bilinéaire, trilinéaire, anisotropique et bien d'autres.
Tous ont besoin d'avoir certaines informations qui sont généralement fournies par les circuits de calcul d'adresse. La première est clairement la partie fractionnaire des coordonnées x,y. La seconde est la dérivée de ces deux coordonnées dans le sens horizontal et vertical., ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Toujours est-il que le filtrage de texture est une opération assez lourde, qui demande beaucoup de calculs arithmétiques. On pourrait en théorie le faire dans les pixels shaders, mais le cout en performance serait absolument insoutenable. Aussi, les cartes graphiques intègrent toutes un circuit dédié au filtrage de texture, le ''texture sampler''. Même les plus anciennes cartes graphiques incorporent une unité de filtrage de texture, ce qui nous montre à quel point cette opération est importante.
[[File:Texture unit.png|centre|vignette|upright=2.0|Unité de texture.]]
On peut configurer la carte graphique de manière à ce qu'elle fasse soit du filtrage bilinéaire, soit du filtrage trilinéaire, on peut configurer le niveau de filtrage anisotropique, etc. Cela peut se faire dans les options de la carte graphique, mais cela peut aussi être géré par l'application. La majorité des jeux vidéos permettent de régler cela dans les options. Ces réglages ne concernent pas la texture elle-même, mais plutôt la manière dont l'unité de texture doit fonctionner. Ces réglages sur l''''état de l'unité de texture''' sont mémorisés quelque part, soit dans l'unité de texture elle-même, soit fournies avec la ressource de texture elle-même, tout dépend de la carte graphique. Certaines cartes graphiques mémorisent ces réglages dans les unités de texture ou dans le processeur de commande, et tout changement demande alors de réinitialiser l'état des unités de texture, ce qui prend un peu de temps. D'autres placent ces réglages dans les ressources de texture elles-mêmes, ce qui rend les modifications de configuration plus rapides, mais demande plus de circuits. D'autres cartes graphiques mélangent les deux options, certains réglages étant globaux, d'autres transmis avec la texture. Bref, difficile de faire des généralités, tout dépend du matériel et le pilote de la carte graphique cache tout cela sous le tapis.
Maintenant que cela est dit, voyons quelles sont les différentes méthodes de filtrage de texture et comment la carte graphique fait pour les calculer.
===Le filtrage au plus proche===
La méthode de filtrage la plus simple consiste à colorier avec le texel le plus proche. Cela revient tout simplement à ne pas tenir compte de la partie fractionnaire des coordonnées x,y, ce qui est très simple à implémenter en matériel. C'est ce que l'on appelle le '''filtrage au plus proche''', aussi appelé ''nearest filtering''.
Autant être franc, le résultat est assez pixelisé et peu agréable à l’œil. Par contre, le résultat est très rapide à calculer, vu qu'il ne demande aucun calcul à proprement parler. Elle ne fait pas appel à la parti fractionnaire des coordonnées entières de texture, ni aux dérivées de ces coordonnées. On peut combiner cette technique avec le mip-mapping, ce qui donne un résultat bien meilleur, bien que loin d'être satisfaisant. Au passage, toutes les techniques de filtrage de texture peuvent se combiner avec du mip-mapping, certaines ne pouvant pas faire sans.
[[File:Interpolation-nearest.svg|centre|vignette|Filtrage de texture au plus proche.]]
===Le filtrage linéaire===
Le filtrage le plus simple est le '''filtrage linéaire'''. Il effectue une interpolation linéaire entre deux mip-maps, deux niveaux de détails. Pour comprendre l'idée, nous allons prendre une situation très simple, avec une texture carrée de 512 texels de côté. Le mip-mapping crée plusieurs textures : une de 256 texels de côté, une de 128 texels, une de 64, etc. Maintenant, la texture est sur un objet à une certaine distance de l'écran, vu de face. Le résultat est qu'elle correspond à l'écran à un carré de 300 pixels de côté (pas d'erreur : pixels, pas texels). Dans ce cas, la texture se trouve entre deux mip-maps : celle de 512 pixels de côté, celle de 256. Laquelle choisir ? Le filtrage au plus proche prend la texture de 512 pixels de côté. Le filtrage linéaire lui, fait autrement.
Vu que la texture est entre deux mip-maps, l'idée est de prendre le texel au plus proche dans chaque texture et de faire une sorte de moyenne appelée l'interpolation linéaire. L'interpolation par du principe que la couleur varie entre les deux texels en suivant une fonction affine, illustrée ci-dessous. Ce ne serait évidemment pas le cas dans le monde réel, mais on supposer cela donne une bonne approximation de ce à quoi ressemblerait une texture à plus haute résolution. On peut alors calculer la couleur du pixel par une simple moyenne pondérée par la distance. Le résultat est que les transitions entre deux niveaux de détails sont plus lisses, moins abruptes.
[[File:Lin interp -é.png|centre|vignette|upright=2.0|Interpolation linéaire.]]
===Le filtrage bilinéaire===
Le filtrage bilinéaire effectue une sorte de moyenne pondérée des quatre texels les plus proches du pixel à afficher. Pour cela, rappelez-vous ce qui a été dit plus haut : les coordonnées x,y d'un pixel ont une partie entière et une partie fractionnaire. Le filtrage au plus proche élimine les parties fractionnaires, ce qui donne une coordonnée x,y. Avec le filtrage bilinéaire, on prend les texels de coordonnées (x,y) ; (x+1,y) ; (x,y+1) ; (x+1,y+1), le pixel étant entre ces 4 texels.
Mais le filtrage ne fait pas qu'une simple moyenne, il prend en compte les parties fractionnaires pour faire la moyenne. En effet, le pixel n'est pas au milieu du carré de texel, il est quelque part mais est souvent plus proche d'un texel que des autres. Et il faut donc pondérer la moyenne par les distances aux 4 texels. Pour cela, la moyenne est calculée à partir d'interpolations linéaires. Avec 4 pixels, nous allons devoir calculer la couleur de deux points intermédiaires. La couleur de ces deux points se calcule par interpolation linéaire, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.
[[File:Bilin3.png|centre|vignette|upright=2|Filtrage bilinéaire de texture.]]
Le circuit qui permet de faire l'interpolation bilinéaire est particulièrement simple. On trouve un circuit de chaque pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu, et un pour la transparence. Chacun de ces circuit est composé de sous-circuits chargés d'effectuer une interpolation linéaire, reliés comme suit.
[[File:Texture sampler unit.png|centre|vignette|Unité de filtrage bilinéaire.]]
Vous noterez que le filtrage bilinéaire accède à 4 pixels en même temps. Fort heureusement, les textures sont stockées de manière à ce qu'on puisse charger les 4 pixels en une fois, comme on l'a vu plus haut. Le filtrage bilinéaire a de fortes chances que les 4 pixels filtrés soient dans la même ''tile'', la seule exception étant quand ils sont tout juste sur le bord d'une ''tile''.
: La console de jeu Nintendo 64 n'utilise que trois pixels au lieu de quatre dans son interpolation bilinéaire, qui en devient une interpolation quasi-bilinéaire. La raison derrière ce choix est une question de performances, comme beaucoup de décisions de ce genre. Le résultat est un rendu imparfait de certaines textures.
===Le filtrage trilinéaire===
Avec le filtrage bilinéaire, des discontinuités apparaissent sur certaines surfaces. Par exemple, pensez à une texture de sol : elle est appliquée plusieurs fois sur toute la surface du sol. A une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512*512 à 256*256, ce qui est visible pour un joueur attentif. De telles transitions sont lissées grâce au filtrage linéaire, il n'y a plus qu'à le combiner avec le filtrage bilinéaire. Rien d’incompatible : le premier filtre l'intérieur d'une mip-map, le second combine deux mip-maps.
Le filtrage trilinéaire prend les deux mip-maps les plus proches, fait un filtrage bilinéaire avec chacune, puis fait une « une moyenne » pondérée entre les deux résultats. Le circuit de filtrage trilinéaire existe en plusieurs versions. La plus simple, illustrée ci-dessous, effectue deux filtrages bilinéaires en parallèle, dans deux circuits séparés, puis combine leurs résultats avec un circuit d'interpolation linéaire. Mais ce circuit nécessite de charger 8 texels simultanément. Qui plus est, ces 8 texels ne sont pas consécutifs en mémoire, car ils sont dans deux niveaux de détails/mip-maps différents.
[[File:Parallel trilinear filtering.png|centre|vignette|upright=2.0|Unité de filtrage trilinéaire parallèle.]]
Vu qu'on lit des texels dans deux mip-maps, les texels sont lus en deux fois : 4 texels provenant de la première mip-map, suivis par les 4 texels de l'autre mip-map. Les 4 premiers texels doivent donc être mis en attente dans des registres, en attendant que les 4 autres arrivent. Une amélioration du circuit précédent gère cela en ajoutant des registres. Il lit les 4 premiers texels, les filtre avec une interpolation bilinéaire, et mémorise le résultat dans un registre. Puis, il lit les 4 autres texels, les filtre, et met le résultat dans un second registre. A ce moment là, un circuit d'interpolation linéaire finit le travail. On économise donc un circuit d'interpolation bilinéaire, sans que les performances soient trop impactées.
[[File:Filtrage trilineaire.png|centre|vignette|upright=1.0|Unité de filtrage trilineaire série.]]
Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le temps où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. À chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients, et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.
===Le filtrage anisotrope===
D'autres artefacts peuvent survenir lors de l'application d'une texture, la perspective pouvant déformer les textures et entraîner l'apparition de flou. La raison à cela est que les techniques de filtrage de texture précédentes partent du principe que la texture est vue de face. Prenez une texture carrée, par exemple. Vue de face, elle ressemble à un carré sur l'écran. Mais tournez la caméra, de manière à voir la texture de biais, avec un angle, et vous verrez que la forme de la texture sur l'écran est un trapèze, pas un carré. Cette déformation liée à la perspective n'est pas prise en compte par les méthodes de filtrage de texture précédentes. Pour le dire autrement, les techniques de filtrage précédentes partent du principe que les 4 texels qui entourent un pixel forment un carré, ce qui est vrai si la texture est vue de face, sans angle, mais ne l'est pas si la texture n'est pas perpendiculaire à l'axe de la caméra. Du point de vue de la caméra, les 4 texels forment un trapèze d'autant moins proche d'un carré que l'angle est grand.
Pour corriger cela, les chercheurs ont inventé le '''filtrage anisotrope'''. En fait, je devrais plutôt dire : LES filtrages anisotropes. Il en existe un grand nombre, dont certains ne sont pas utilisés dans les cartes graphiques actuelles, soit car ils trop gourmand en accès mémoires et en calculs pour être efficaces, soit car ils ne sont pas pratiques à mettre en œuvre. Il est très difficile de savoir quelles sont les techniques de filtrage de texture utilisées par les cartes graphiques, qu'elles soient récentes ou anciennes. Beaucoup de ces technologies sont brevetées ou gardées secrètes, et il faudrait vraiment creuser les brevets déposés par les fabricants de GPU pour en savoir plus. Les algorithmes en question seraient de plus difficiles à comprendre, les méthodes mathématiques cachées derrière ces méthodes de filtrage n'étant pas des plus simple.
[[File:Anisotropic filtering en.png|centre|vignette|upright=2|Exemple de filtrage anisotrope.]]
==La compression de textures==
Les textures les plus grosses peuvent aller jusqu'au mébioctet, ce qui est beaucoup. Pour limiter la casse, les textures sont compressées. La '''compression de texture''' réduit la taille des textures, ce qui peut se faire avec ou sans perte de qualité. Elle entraîne souvent une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande. Mais il s'agit là d'une technique très simple, beaucoup plus simple que les techniques que nous allons voir dans cette section. Nous allons voir quelque algorithmes de compression de textures de complexité intermédiaire, mais n'allons pas voir l'état de l'art. Il existe des formats de texture plus récents que ceux qui nous allons aborder, comme l{{'}}''Ericsson Texture Compression'' ou l{{'}}''Adaptive Scalable Texture Compression'', plus complexes et plus efficaces.
Notons que les textures sont compressées dans les fichiers du jeu, mais aussi en mémoire vidéo. Les textures sont décompressées lors de la lecture. Pour cela, la carte graphique contient alors un circuit, capable de décompresser les textures lorsqu'on les lit en mémoire vidéo. Les cartes graphiques supportent un grand nombre de formats de textures, au niveau du circuit de décompression. Du fait que les textures sont décompressées à la volée, les techniques de compression utilisées sont assez particulières. La carte graphique ne peut pas décompresser une texture entière avant de pouvoir l'utiliser dans un ''pixel shader''. A la place, on doit pouvoir lire un morceau de texture, et le décompresser à la volée. On ne peut utiliser les méthodes de compression du JPEG, ou d'autres formats de compression d'image. Ces dernières ne permettent pas de décompresser une image morceau par morceau.
Pour permettre une décompression/compression à la volée, les textures sont des textures tilées, généralement découpées en tiles de 4 * 4 texels. Les ''tiles'' sont compressées indépendamment les unes des autres. Et surtout, avec ou sans compression, la position des tiles en mémoire ne change pas. On trouve toujours une tile tous les T octets, peu importe que la tile soit compressée ou non. Par contre, une tile compressée n'occupera pas T octets, mais moins, là où une tile compressée occupera la totalité des T octets. En clair, compresser une tile fait qu'il y a des vides entre deux tiles dans al mémoire vidéo, mais ne change rien à leur place en mémoire vidéo qui est prédéterminée, peu importe que la texture soit compressée ou non. L'intérêt de la compression de textures n'est pas de réduire la taille de la texture en mémoire vidéo, mais de réduire la quantité de données à lire/écrire en mémoire vidéo. Au lieu de lire T octets pour une tile non-compressée, on pourra en lire moins.
===La palette indicée et la technique de ''Vector quantization''===
La technique de compression des textures la plus simple est celle de la '''palette indicée''', que l'on a entraperçue dans le chapitre sur les cartes d'affichage. La technique de '''''vector quantization''''' peut être vue comme une amélioration de la palette, qui travaille non pas sur des texels, mais sur des ''tiles''. À l'intérieur de la carte graphique, on trouve une table qui stocke toutes les ''tiles'' possibles. Chaque ''tile'' se voit attribuer un numéro, et la texture sera composé d'une suite de ces numéros. Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l’embarqué utilisent ce genre de compression.
===Les algorithmes de ''Block Truncation coding''===
La première technique de compression élaborée est celle du '''''Block Truncation Coding''''', qui ne marche que pour les images en niveaux de gris. Le BTC ne mémorise que deux niveaux de gris par ''tile'', que nous appellerons couleur 1 et couleur 2, les deux niveaux de gris n'étant pas le même d'une ''tile'' à l'autre. Chaque pixel d'une ''tile'' est obligatoirement colorié avec un de ces niveaux de gris. Pour chaque pixel d'une ''tile'', on mémorise sa couleur avec un bit : 0 pour couleur 1, et 1 pour couleur 2. Chaque ''tile'' est donc codée par deux entiers, qui codent chacun un niveau de gris, et une suite de bits pour les pixels proprement dit. Le circuit de décompression est alors vraiment très simple, comme illustré ci-dessous.
[[File:Block Truncation coding.jpg|centre|vignette|upright=2.0|Block Truncation coding.]]
La technique du BTC peut être appliquée non pas du des niveaux de gris, mais pour chaque composante Rouge, Vert et Bleu. Dans ces conditions, chaque ''tile'' est séparée en trois sous-''tiles'' : un sous-bloc pour la composante verte, un autre pour le rouge, et un dernier pour le bleu. Cela prend donc trois fois plus de place en mémoire que le BTC pur, mais cela permet de gérer les images couleur.
===Le format de compression S3TC / DXTC===
L'algorithme de '''Color Cell Compression''', ou CCC, améliore le BTC pour qu'il gère des couleurs autre que des niveaux de gris. Ce CCC remplace les deux niveaux de gris par deux couleurs. Une ''tile'' est donc codée avec un entier 32 bits par couleur, et une suite de bits pour les pixels. Le circuit de décompression est identique à celui utilisé pour le BTC.
[[File:Color Cell Compression.jpg|centre|vignette|Color Cell Compression.]]
[[File:Dxt1-memory-layout.png|vignette|Dxt1 et ''color cell compression''.]]
Le format de compression de texture utilisé de base par Direct X, le DXTC, est une version amliorée de l'algorithme précédent. Il est décliné en plusieurs versions : DXTC1, DXTC2, etc. La première version du DXTC est une sorte d'amélioration du CCC : il ajoute une gestion minimale de transparence, et découpe la texture à compresser en ''tiles'' de 4 pixels de côté. La différence, c'est que la couleur finale d'un texel est un mélange des deux couleurs attribuée au bloc. Pour indiquer comment faire ce mélange, on trouve deux bits de contrôle par texel.
Si jamais la couleur 1 < couleur2, ces deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (2 * Couleur1 + Couleur2) / 3
* 11 = (Couleur1 + 2 * Couleur2) / 3
Sinon, les deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (Couleur1 + Couleur2) / 2
* 11 = Transparent
[[File:DXTC.jpg|centre|vignette|DXTC.]]
Le circuit de décompression du DXTC ressemble alors à ceci :
[[File:Circuit de décompression du DXTC.jpg|centre|vignette|upright=2.0|Circuit de décompression du DXTC.]]
===Les format DXTC 2, 3, 4 et 5 : l'ajout de la transparence===
Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3, lui-même replacé par le DXT4 et par le DXT5. Dans le DXT3, la transparence fait son apparition. Pour cela, on ajoute 64 bits par ''tile'' pour stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1.
[[File:Dxt23-memory-layout.png|centre|vignette|Dxt 2 et 3.]]
Dans le DXT4 et le DXT5, la méthode utilisée pour compresser les couleurs l'est aussi pour les valeurs de transparence. L'information de transparence est stockée par un en-tête contenant deux valeurs de transparence, le tout suivi d'une matrice qui attribue trois bits à chaque texel. En fonction de la valeur des trois bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1.
[[File:Dxt45-memory-layout.png|centre|vignette|Dxt 4 et 5.]]
===Le format de compression PVRTC===
Passons maintenant à un format de compression de texture un peu moins connu, mais pourtant omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle travaille surtout dans les cartes graphiques embarquées. Ses cartes se trouvent notamment dans l'ipad, l'iPhone, et bien d'autres smartphones actuels.
Avec le PVRTC, les textures sont encore une fois découpées en ''tiles'' de 4 texels par 4, mais la ressemblance avec le DXTC s’arrête là. Chacque ''tile'' est codée avec :
* une couleur codée sur 16 bits ;
* une couleur codée sur 15 bits ;
* 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
* et un bit de modulation, qui permet de configurer l’interprétation des bits de mélange.
Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4*4.
==Annexe : le ''normal-mapping'' hardware==
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, parlons un peu du ''normal mapping'' et de son implémentation dans les unités de texture. Pour rappel, 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. Et pour comprendre quel est le rapport avec les textures, nous allons devoir faire quelques rappels sur l'éclairage par pixel.
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
L'éclairage se fait en utilisant de nombreux calculs, qu'on a détaillé dans le chapitre sur les bases du rendu 3D. Ceux-ci utilisent la normale d'un sommet, à savoir un vecteur orienté à la verticale de la surface. En théorie, il y a une normale par sommet, ce qui fait que les calculs d'éclairage doivent se faire au niveau géométrique, avant la rastérisation. Mais pour obtenir un éclairage de meilleure qualité, il y a des techniques qui calcule l'éclairage d'une scène 3D pixel par pixel.
Une première technique d'éclairage par pixel interpole les normales lors de l'étape de rastérisation. On obtient alors un éclairage de Phong. Une autre solution est celle du ''normal mapping''. Le '''''normal mapping''''' précalcule les normales d'une surface dans une texture, appelée la ''normal map''. L'éclairage est alors réalisé par un pixel shader, qui lit les normales depuis cette texture et fait les calculs d'éclairage avec.
Notons que l'accès à la ''normal map'' se fait comme pour n'importe quelle texture : les texels sont lus en mémoire vidéo, puis le filtrage de texture est appliqué, et enfin le tout est envoyé au ''pixel shader''. En théorie, le filtrage effectue une sorte d'interpolation des normales automatique, ce qui permet de trouver la normale à un pixel précis, même s'il n'est pas sur un texel.
La ''normal map'' est une texture, donc. Mais ce n'est pas une texture comme une autre. Elle mémorise un vecteur pour chaque texel, pas une couleur. Il est possible d'utiliser les formats de textures vus plus haut, même si cela ne donne pas de bons résultats. Après tout, un vecteur est codé par trois coordonnées x,y,z, il est possible de coder chacune avec un octet et de packager cela dans une texture RGB classique. La coordonnée x va dans la composante Rouge, la coordonnée Y dans la composante Bleu, et la coordonnée z dans la composante z.
Le problème est que le rendu final n'est pas très beau. Et les opérations de filtrage ne donnent pas de très bons résultats. Pour remédier à ces problèmes, des formats de texture spécialisés pour les ''normal map'' ont été inventés. Le premier d'entre eux est le '''3Dc''', aussi appelé BC5 pour ''Block Compression 5''. Il utilise un octet par texel, au lieu de trois avec une texture RGB normale non-compressée.
Avec le 3Dc, les trois coordonnées des vecteurs ne sont pas mémorisées, seules deux d'entre elles le sont. En effet, les normales sont des vecteurs ''normalisés'', c'est à dire que leur longueur vaut 1, par construction. Vu que la taille est connue à l'avance, on peut en déduire la coordonnée z à partir des coordonnées x et y, avec l'usage du théorème de Pythagore. Par définition, <math>z^2 = 1 - x^2 - y^2</math>. Le calcul peut être fait dans le ''pixel shader'' sans problème.
Pour le reste, la compression de la ''normal map'' ressemble un peu à celle des textures. La ''normal map'' est découpée en blocs de 4 par 4 texels de côté, chacune étant encodée en tenant compte de certaines redondances. Dans un bloc, les coordonnées x sont dans un certain intervalle, allant de <math>x_{min}</math> à <math>x_{max}</math>, idem pour la coordonnée y qui est dans l'intervalle <math>y_{min}</math>, <math>y_{max}</math>. L'intervalle <math>x_{min}</math> à <math>x_{max}</math> est coupé en 8 parts égales, ce qui fait 8 valeurs possibles dans cet intervalle, chacune correspondant à une valeur x possible pour une normale du bloc. La normale est donc encodée en précisant quelle valeur est la bonne, en utilisant un indice de 3 bits pour cela. La valeur exacte de la coordonnée x se calcule à partir de l'indice comme suit :
: <math>x = x_{min} + \left( \frac{x_{min} - x_{min}}{8} \right) \times \text{indice} </math>
Pour encoder un bloc de 4 pr 4 normales, il faut donc :
* Un octet pour chaque coordonnée <math>x_{min}</math>, <math>x_{max}</math>, <math>y_{min}</math>, <math>y_{max}</math>.
* 6 bits par normale : 3 pour encoder le décalage de la coordonnée x, 3 pour la coordonnée y.
==Annexe : les ''shadowmap'' hardware==
Les anciens GPU, notamment la Geforce FX, avaient des fonctionnalités spécifiques pour le calcul des ombres. Dans la plupart des jeux vidéos de l'époque, et même de maintenant, les ombres sont calculées avec la technique des ''shadowmap''. L'idée est assez simple sur le principe : un pixel est dans l'ombre quand il est invisible depuis une source de lumière.
L'idée est que le rendu est réalisé en plusieurs passes, avec une passe par source de lumière et une passe finale pour calculer l'image finale. Nous allons expliquer la technique avec une seule source de lumière, et allons utiliser l'exemple de la scène ci-dessous.
[[File:7fin.png|centre|vignette|Scène 3D d'exemple.]]
===La technique du ''shadowmapping''===
[[File:2shadowmap.png|vignette|Résultat de la première passe : ''shadowmap''..]]
La première passe rend l'image depuis le point de vue de la source de lumière. Cette première passe ne rend pas les couleurs de la scène, elle ne s'intéresse qu'à la profondeur des pixels. Le résultat est que l'image ne rend que le tampon de profondeur. Celui-ci est ensuite réutilisé comme texture pour la passe suivante. La texture en question est appelée la '''''shadownmap'''''.
La perspective utilisée, ainsi que le ''view frustrum'', dépend de la source de lumière. Pour une source de lumière qui émet un cône de lumière, le ''view frustrum'' de l'image rendue doit contenir tout le cone de lumière, et doit coller le plus possible à celui-ci. Pour une source directionnelle, comme le soleil, une perspective orthographique est utilisée.
La seconde passe rend l'image du point de vue de la caméra, pour rendre l'image finale. Elle rend l'image finale, qui est composée de pixels, chacun ayant une position à l'écran x,y, et une profondeur z. Les coordonnées sont transformées pour obtenir la position de ce pixel depuis le point de vue de la caméra. Une simple multiplication de matrice suffit, rien de bien compliqué, un shader peut le faire.
[[File:5failed.png|vignette|Résultat du test des comparaisons.]]
Après cette étape, on a alors les coordonnées x,y,z de ce pixel du point de vue de la caméra, et la ''shadowmap''. Il est alors possible d'accéder à la ''shadowmap'' au même endroit, à la même place que le pixel testé, aux mêmes coordonnées x,y. Si la profondeur du pixel est supérieure à celle de la shadowmap au même endroit, alors le pixel est situé derrière la surface visible, donc est dans l'ombre. Sinon, il n'est pas dans l'ombre. Le même procédé est répété sur chaque pixel de l'écran.
===Les optimisations hardware du ''shadowmapping''===
La technique des ''shadowmap'' demande donc de calculer une texture ''shadowmap'', puis de lire celle-ci et de faire des comparaisons de profondeur. Les GPU comme la Geforce FX intégraient du matériel dans les unités de texture pour faciliter ce travail. Les unités de texture pouvaient lire les ''shadowmap'', et faire la comparaison de profondeur toutes seules, elles avaient des circuits pour. Il suffisait de leur fournir le pixel à tester, ses coordonnées x,y,z, et l'adresse de la ''shadowmap''. Les unités de texture renvoyaient alors un résultat valant 0 ou 1 : 1 si le pixel est dans l'ombre, 0 sinon.
Elles pouvaient même effectuer du filtrage de texture sur les ''shadowmap''. Mais le filtrage était différent de celui utilisé sur les autres textures : moyenner des valeurs de profondeur ne marche pas bien. Elles utilisaient des techniques de filtrage différentes : elles faisaient les tests de comparaison, puis faisaient la moyenne des résultats. Ainsi, pour du filtrage bilinéaire, elles lisaient 4 texels dans la ''shadowmap'', puis faisaient 4 tests de comparaison, et moyennaient les 4 résultats.
==Annexe : les textures virtuelles==
Les '''textures virtuelles''' sont une optimisation des textures normales, qui visent à accélérer le rendu de terrains de grande taille. Imaginez par exemple un monde assez ouvert, comme un environnement en forêt ou en montagne, avec une grande distance de visibilité. Avec de tels terrains, le "sol" est recouvert par une texture de sol unique qui recouvre tout le terrain. Elle ne se répète pas, est de très grande taille, et peut parfois recouvrir toute la map ! Mais il n'y a pas assez de mémoire vidéo pour mémoriser la texture toute entière. La seule solution est la suivante : une partie de la texture est placée en mémoire vidéo, le reste est soit placé en mémoire RAM ou sur le disque dur.
Pour cela, le moteur de jeu utilise une optimisation ingénieuse, basée sur une observation assez basique : une bonne partie de la texture est visible, mais le reste est caché par des arbres, des habitations ou d'autres obstacles. Une optimisation possible de ne garder en mémoire vidéo que les portions visibles de la texture, pas les portions cachées. Une autre optimisation mélange textures virtuelles et ''mip-mapping''. L'idée est que pour les portions lointaines d'une texture, la texture utilisée est une ''mip-map'' de basse résolution. L'idée est alors de ne charger que la ''mip-map'' adéquate, pas les autres niveaux de détail. En clair, la texture de base n'est pas chargée en mémoire vidéo, mais la ''mip-map'' basse résolution l'est.
===Une texture à deux niveaux===
L'implémentation des textures virtuelles découpe les méga-textures en ''tiles'', en morceaux rectangulaires de taille modeste. En clair, le terrain est découpé en morceau rectangulaires/carrés. Seules les tiles nécessaires sont chargées en mémoire vidéo, pas les autres. Par exemple, les ''tiles'' non-visibles ne sont pas placées en mémoire vidéo, seules les ''tiles'' visibles le sont. De même, il y a une ''tile'' par niveau de mip-map : seul la tile correspondant le niveau adéquat est en mémoire vidéo, les autres niveaux de détail ne sont pas chargés. On peut faire une analogie avec la mémoire virtuelle, où les données sont découpées en pages, qui sont chargées en mémoire RAM à la demande, suivant les besoins, les données pouvant être swappées sur le disque dur si elles sont peu utilisées. Sauf qu'ici, il s'agit de textures qui sont découpées en pages chargées à la demande en mémoire vidéo, depuis la RAM système.
Une texture virtuelle est en réalité un système à deux niveaux : une liste de ''tiles'' et les ''tiles'' elles-mêmes. La liste de ''tiles'' est appelée un '''atlas de texture''', c'est un peu l'équivalent de la ''tilemap'' pour le rendu 2D. Rendre une texture demande de calculer quelle ''tile'' contient le texel à afficher, consulter la ''tile'' en question, puis récupérer le texel adéquat dans cette ''tile''. La ''tile'' est donc une texture, mais la texture à charger est choisie parmi un ensemble, qui est ici l'atlas de texture.
===L'implémentation : logicielle versus matérielle===
Les textures virtuelles ont été utilisées pour la première fois par les jeux Rage 1 et 2 d'IdSoftware, et quelques jeux ultérieurs comme DOOM 2016. IdSoftware les appelait des '''''mega-textures'''''. L'optimisation permettait des gains en performance assez impressionnants. Le jeu Rage 1 utilisait une texture carrée unique de 128k pixels de côté pour rendre le terrain. En théorie, une telle texture devrait prendre 64 giga-octets, mais le jeu tournait correctement avec 512 méga-octets de RAM, poussivement avec seulement 256 méga-octets de RAM.
De nos jours, les textures virtuelles sont supportées par beaucoup de jeux vidéos, les moteurs les plus courants gèrent de telles textures de manière logicielles. Mais quelques GPU récents supportent les textures virtuelles. Sur les GPU récents, l'atlas de texture est géré nativement par le matériel. Le GPU choisit quelle ''tile'', quelle texture choisir pour rendre le texel adéquat. Pour cela, le GPU calcule quelle ''tile'' charger, consulte l'atlas de texture, et lit la texture de ''tile'' adéquate.
Mais l'implémentation sur les GPU récents a de nombreuses limitations. La limitation la plus importante est que la taille des textures virtuelles ne peut pas dépasser la taille d'une texture normale, soit 32768 pixels de côté pour une texture carrée environ sur les GPU de 2020. De plus, le chargement d'une ''tile'' est très lent. En clair, dès qu'on veut changer de niveau de mip-map pour une tile, ou dès qu'une tile devient visible, le chargement de la tile peut facilement prendre plusieurs centaines de millisecondes. Le filtrage de texture est très complexe avec des textures virtuelles, ce qui fait que le filtrage de texture virtuelle est souvent soumis à des limitations que les textures normales n'ont pas, notamment pour le filtrage anisotropique.
{{NavChapitre | book=Les cartes graphiques
| prev=Le rasterizeur
| prevText=Le rasterizeur
| next=Les Render Output Target
| nextText=Les Render Output Target
}}{{autocat}}
1sluxlr7mcmsx03uus6wxo0uzjn9mvj
763408
763407
2026-04-10T17:08:15Z
Mewtow
31375
/* Annexe : le normal-mapping hardware */
763408
wikitext
text/x-wiki
[[File:Texture mapping.png|vignette|''Texture mapping'']]
Les '''textures''' sont des images que l'on va plaquer sur la surface d'un objet, du papier peint en quelque sorte. Les cartes graphiques supportent divers formats de textures, qui indiquent comment les pixels de l'image sont stockés en mémoire : RGB, RGBA, niveaux de gris, etc. Une texture est donc composée de "pixels", comme toute image numérique. Pour bien faire la différence entre les pixels d'une texture, et les pixels de l'écran, les pixels d'une texture sont couramment appelés des ''texels''.
==Le placage de textures inverse==
Pour rappel, plaquer une texture sur un objet consiste à attribuer un texel à chaque sommet, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture.
Dans les faits, on n'utilise pas de coordonnées entières de ce type. Les coordonnées de texture sont deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. Le nom donnée à cette technique de description des coordonnées de texture s'appelle l''''''UV Mapping'''''.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Les API 3D modernes gèrent des textures en trois dimensions, ce qui ajoute une troisième coordonnée de texture notée w. Dans ce qui va suivre, nous allons passer les textures en trois dimensions sous silence. Elles ne sont pas très utilisées, la quasi-totalité des jeux vidéo et applications 3D utilisant des textures en deux dimensions. Par contre, le matériel doit gérer les textures 3D, ce qui le rend plus complexe que prévu. Il faut ajouter quelques circuits pour, de quoi gérer la troisième coordonnée de texture, etc.
Lors de la rastérisation, chaque fragment se voit attribuer un sommet, et donc la coordonnée de texture qui va avec. Si un pixel est situé pile sur un sommet, la coordonnée de texture de ce sommet est attribuée au pixel. Si ce n'est pas le cas, la coordonnée de texture finale est interpolée à partir des coordonnées des trois sommets du triangle rastérisé. L'interpolation en question a lieu dans l'étape de rastérisation, comme nous l'avons vu dans le chapitre précédent. Le fait qu'il y ait une interpolation fait que les coordonnées du pixel gagent à être des nombres flottants. On pourrait faire une interpolation avec des coordonnées de texture entières, mais les arrondis et autres imprécisions de calcul donneraient un résultat graphiquement pas terrible, et empêcheraient d'utiliser les techniques de filtrage de texture que nous verrons dans ce chapitre.
À partir de ces coordonnées de texture, la carte graphique calcule l'adresse du texel qui correspond, et se charge de le lire. Et toute la magie a lieu dans ce calcul d'adresse, qui part de coordonnées de texture flottante, pour arriver à une adresse mémoire. Le calcul de l'adresse du texel se fait en plusieurs étapes, que nous allons voir ci-dessous. La première étape convertit les coordonnées flottantes en coordonnées entières, qui disent à quel ligne et colonne se trouve le texel voulu dans la texture. L'étape suivante transforme ces coordonnées x,y entières en adresse mémoire.
===La normalisation des coordonnées===
J'ai dit plus haut que les coordonnées de texture sont des coordonnées flottantes, comprises entre 0 et 1. Mais il faut savoir que les pixels shaders peuvent modifier celles-ci pour mettre en œuvre certains effets graphiques. Et le résultat peut alors se retrouver en-dehors de l'intervalle 0,1. C'est quelque chose de voulu et qui est traité par la carte graphique automatiquement, sans que ce soit une erreur. Au contraire, la manière dont la carte graphique traite cette situation permet d'implémenter des effets graphiques comme des textures en damier ou en miroir.
[[File:Clamp tile.jpg|vignette|Clamp tile]]
Il existe globalement trois méthodes très simples pour gérer cette situation, qui sont appelés des '''modes d'adressage de texture'''.
* La première méthode est de faire en sorte que le résultat sature. Si une coordonnée est inférieur à 0, alors on la remplace par un zéro. Si elle est supérieure à 1, on la ramène à 1. Avec cette méthode, tout se passe comme si les bords de la texture étaient étendus et remplissaient tout l'espace autour de la texture. Le tout est illustré ci-dessous. Ce mode d'accès aux textures est appelé le '''''clamp'''''.
* Une autre solution retire la partie entière de la coordonnée, elle coupe tout ce qui dépasse 1. Pour le dire autrement, elle calcule le résultat modulo 1 de la coordonnée. Le résultat est que tout se passe comme si la texture était répétée à l'infini et qu'elle pavait le plan.
* Une autre méthode remplit les coordonnées qui sortent de l’intervalle 0,1 avec une couleur préétablie, configurée par le programmeur.
===La conversion des coordonnées de textures flottantes en adresse mémoire===
Une fois la normalisation effectuée, les coordonnées de texture sont utilisées pour lire le texel voulu. Pour cela, les coordonnées de texte sont transformées en adresse mémoire, adresse qui pointe sur le texel ayant ces cordonnées. Pour cela, la première étape est de transformer les coordonnées flottantes u,v en coordonnées entières x,y qui pointent sur un texel. Pour cela, il suffit de multiplier les coordonnées flottantes u,v par la résolution de la texture accédée. Pour un écran de résolution <math>\text{height,width}</math>, le calcul est le suivant :
: <math>x = u \times \text{width}</math>
: <math>y = v \times \text{height}</math>
Le résultat est un nombre avec une partie entière et une partie fractionnaire. La partie entière des deux coordonnées donne la position x,y voulue, et la partie fractionnaire est conservée pour le filtrage de textures, mais passons cela sous silence pour le moment.
La seconde étape prend les coordonnées entières x,y et calcule l'adresse mémoire du texel. L'adresse dépend de la position de la texture en mémoire, précisément de son début, son premier texel, mais aussi de la position du texel par rapport au début de la texture. Et calculer cette position intra-texture dépend de la manière dont les texels sont stockés en mémoire.
====Les textures naïves====
Les programmeurs qui lisent ce cours s'attendent certainement à ce que la texture soit stockée en mémoire ligne par ligne, ou colonne par colonne. Cela veut dire que le premier pixel en partant d'en haut à gauche est stocké en premier, puis celui immédiatement à sa droite, puis celui encore à droite, et ainsi de suite. Une fois qu'on arrive à la fin d'une ligne, on passe à la ligne suivante, en-dessous. Cette organisation ligne par ligne s'appele l'organisation '''''row major order'''''. On peut faire pareil, mais colonne par colonne, ce qui donne le '''''column major order'''''.
[[File:Speicheranordnung Feld.svg|centre|vignette|upright=2|Row et column major order.]]
Maintenant, supposons que la texture commence à l'adresse <math>A_\text{texture}</math>, qui est l'adresse du premier texel. La texture a une résolution de <math>\text{width}</math> texels de large et <math>\text{height}</math> texels de haut. Par définition, les coordonnées X et Y des texels commencent à 0, ce qui fait que le pixel en haut à gauche a les coordonnées 0,0.
L'adresse du pixel se calcule comme suit :
: <math>A_\text{pixel} = A_\text{texture} + (\text{taille d'une ligne en octets} \times Y) + (\text{taille d'un texel en octets} \times X)</math>
La taille d'un pixel en mémoire est notée T. La taille d'une ligne en mémoire est de <math>width \times T</math>, par définition, vu qu'elle fait <math>width</math> texels. On a donc :
: <math>A_\text{pixel} = A_\text{texture} + (width \times T \times Y) + (T \times X)</math>
La formule se réécrit comme suit :
: <math>A_\text{pixel} = A_\text{texture} + T \times (width \times Y + X)</math>
Le calcul d'adresse est donc assez simple. Malheureusement, les textures ne sont pas stockées de cette manière en mémoire vidéo. En effet, elle se marie mal avec les opérations de filtrage de texture que nous allons voir dans ce qui suit. Le filtrage d'un texel dépend de ses voisins du dessus et du dessous. Le fait que la texture n'est pas forcément parcourue ligne par ligne fait que stocker une texture ligne par ligne n'est pas l'idéal.
De même, les textures sont déformées par la perspective. L'affichage de la texture ne se fait alors pas ligne par ligne, mais en parcourant la texture en diagonale, l'angle de la diagonale correspondant approximativement à l'angle que fait la verticale de la texture avec le regard. Vu qu'on ne connait pas à l'avance l'angle que fera la diagonale de parcours, on doit ruser.
====Les textures tilées====
Une première solution à ce problème est celle des '''textures tilées'''. Avec ces textures, l'image de la texture est découpée en ''tiles'', des rectangles ou en carrés de taille fixe, généralement des carrés de 4 pixels de côté. Les tiles ont une largeur et une longueur égales, afin de simplifier les calculs : on divise X et Y par le même nombre. De plus, leur largeur et leur longueur sont une puissance de deux, afin de simplifier les calculs d'adresse. Les ''tiles'' sont alors mémorisée les unes après les autres dans le fichier de la texture.
[[File:Texture tilée.png|centre|vignette|upright=2|Texture tilée]]
La formule de calcul d'adresse vue plus haut doit être adaptée pour tenir compte des tiles. Pour cela, il faut remplacer la taille d'un texel par la taille d'une tile, et que la largeur de la texture soit exprimée en nombre de tiles. De plus, on doit adapter les coordonnées des texels pour donner des coordonnées de tile. Généralement, les tiles sont des carrés de N pixels de côté, ce qui fait qu'on peut regrouper les lignes et les colonnes par paquets de N. Il suffit donc de diviser Y et X pour obtenir les coordonnées de la tile, de même que la larguer. La formule pour calculer la position de la énième tile est alors la suivante :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + \text{Taille mémoire d'une tile} \times \left( {\text{Width} \over N} \times {Y \over N} + {X \over N} \right)</math>
On peut réécrire le tout comme suit :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + K \times \left( {Y \over N} + X \right)</math>, avec K une constante connue à la compilation des shaders.
Vu que les tiles sont carrées avec une largeur qui est une puissance de deux, la multiplication par la taille d'une tile en mémoire se simplifie : on passe d'une multiplication entière à des décalages de bits. Même chose pour le calcul de l'adresse de la tile à partir des coordonnées x,y : ils impliquent des divisions par une puissance de deux, qui deviennent de simples décalages.
La position d'un pixel dans une tile dépend du format de la texture, mais peut se calculer avec quelques calculs arithmétiques simples. Dans les cas les plus simples, les pixels sont mémorisés ligne par ligne, ou colonne par colonne. Mais ce n'est pas systématiquement le cas. Toujours est-il que les calculs pour déterminer l'adresse sont simples, et ne demandent que quelques additions ou multiplications. Mais avec les formats de texture utilisés actuellement, les tiles sont chargées en entier dans le cache de texture, sans compter que diverses techniques de compression viennent mettre le bazar, comme on le verra dans la suite de cours.
Un avantage de l'organisation en tiles est qu'elle se marie bien avec le parcours des textures. On peut parcourir une texture dans tous les sens, horizontal, vertical, ou diagonal, on sait que les prochains pixels ont de fortes chances d'être dans la même tile. Si on rentre dans une tile par la gauche en haut, on a encore quelques pixels à parcourir dans la tile, par exemple. De même, le filtrage de textures est facilité. On verra dans ce qui va suivre que le filtrage de texture a besoin de lire des blocs de 4 texels, des carrés de 2 pixels de côté. Avec l'organisation en tile, on est certain que les 4 texels seront dans la même tile, sauf s'ils ont le malheur d'être tout au bord d'une tile. Ce dernier cas est assez rare, et il l'est d'autant plus que les tiles sont grandes. Enfin, un dernier avantage est que les tiles sont généralement assez petites pour tenir tout entier dans une ligne de cache. Le cache de texture est donc utilisé à merveille, ce qui rend les accès aux textures plus rapides.
====Les textures basées sur des ''z-order curves''====
Les formats de textures théoriquement optimaux utilisent une '''''Z-order curve''''', illustrée ci-dessous. L'idée est de découper la texture en quatre rectangles identiques, et de stocker ceux-ci les uns à la suite des autres. L'intérieur de ces rectangles est lui aussi découpé en quatre rectangles, et ainsi de suite. Au final, l'ordre des pixels en mémoire est celui illustré ci-dessous.
[[File:Z-CURVE.svg|centre|vignette|upright=2|Construction d'une ''Z-order curve''.]]
Les texels sont stockés les uns à la suite des autres dans la mémoire, en suivant l'ordre donnée par la ''Z-order curve''. Le calcul d'adresse calcule la position du texel en mémoire, par rapport au début de la texture, et ajoute l'adresse du début de la texture. Mais tout le défi est de calculer la position d'un texel en mémoire, à partir des coordonnées x,y. Le calcul peut sembler très compliqué, mais il n'en est rien. Le calcul demande juste de regarder les bits des deux coordonnées et de les combiner d'une manière particulièrement simple. Il suffit de placer le bit de poids fort de la coordonnée x, suivi de celui de la coordonnée y, et de faire ainsi de suite en passant aux bits suivants.
[[File:Zcurve45bits.png|centre|vignette|upright=1.5|Calcul de la position d'un élément dans une ''Z-order curve'' à partir des coordonnées x et y.]]
L'avantage d'une telle organisation est que la textures est découpées en ''tiles'' rectangulaires d'une certaine taille, elles-mêmes découpées en ''tiles'' plus petites, etc. Et il se trouve que cette organisation est parfaite pour le cache de texture. L'idéal pour le cache de texture est de charger une ''tile'' complète dans le cache de textures. Quand on accède à un texel, on s'assure que la ''tile'' complète soit chargée. Mais cela demande de connaitre à l'avance la taille d'une ''tile''. Les formats de texture fournissent généralement une ''tile'' carré de 4 pixels de côté, mais cela donnerait un cache trop petit pour être vraiment utile. Avec cette méthode, on s'assure qu'il y ait une ''tile'' avec la taille optimale. Les ''tiles'' étant découpées en ''tiles'' plus petites, elles-mêmes découpées, et ainsi de suite, on s'assure que la texture est découpées en ''tiles'' de taille variées. Il y aura au moins une ''tile'' qui rentrera tout pile dans le cache.
==Les techniques de rendu à textures multiples==
Nous venons de voir comment une texture est plaquée sur un objet 3D, ou une surface comme un sol. Pour résumer, le calcul de l'adresse d'un texel prend la position du texel par rapport au début de la texture, et ajoute l'adresse du début de la texture. L'adresse mémoire de la texture est connue au moment où le pilote de la carte graphique place la texture dans la mémoire vidéo, et cette information est transmise au matériel par l'intermédiaire du processeur de commande, puis passée aux processeurs de shaders et à l'unité de texture. Le tout est couplé à d'autres informations, la plus importante étant la ''taille de la texture en octets'', pour éviter de déborder lors des accès à la texture.
Néanmoins, il s'agit là du cas le plus simple. Certaines techniques de rendu demandent de choisir la texture à plaquer parmi un ensemble de plusieurs textures. Les techniques en question sont assez variées et n'ont pas grand chose en commun. Les plus connues sont le ''mip-mapping'', le ''cube-mapping'' et les textures virtuelles. Le ''mip-mapping'' sert à filtrer les textures, chose qu'on expliquera plus tard, le ''cube-mapping'' sert à simuler des réflexions sur un objet en plaquant une texture de l'environnement dessus, les textures virtuelles sont une optimisation pour les textures des terrains de grande taille. Mais malgré leurs différences, elles demandent de choisir quelle texture plaquer entre plusieurs textures de base. En clair, l'adresse de base de la texture varie selon la situation. Voyons-les dans le détail.
===Le mip-mapping===
Le '''mip-mapping''' a pour but de légèrement améliorer les graphismes des objets lointains, tout en rendant les calculs de texture plus rapides. Formellement, le ''mip-mapping'' est une technique de filtrage de texture, mais nous l'abordons maintenant car elle est surtout liée au calcul d'adresse. Les unités de texture ont des circuits de filtrage de texture séparés des circuits de ''mip-mapping'' et de calcul d'adresse, d'où le fait que nous en parlons séparément.
Le problème résolu par le ''mip-mapping'' est le rendu des textures lointaines. Si une texture est plaquée sur un objet lointain, une bonne partie des détails est invisible pour l'utilisateur. Un pixel de l'écran est associé à plusieurs texels. Idéalement, la carte graphiques devrait lire tous ces texels et en faire une sorte de moyenne pondérée, pour calculer la couleur finale du pixel. Mais dans les faits, ce serait très gourmand et compliqué à implémenter en hardware. Une solution serait de ne garder que quelque texels, mais cela a tendance à créer des artefacts visuels (les textures affichées ont tendance à pixeliser). Le ''mip-mapping'' permet de réduire ces deux problèmes en même temps en précalculant cette moyenne pondérée pour des distances prédéfinies.
L'idée est d'utiliser plusieurs exemplaires d'une même texture à des résolutions différentes, chaque exemplaire étant adapté à une certaine distance. Par exemple, une texture sera stocké avec un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre de 128 * 128 et ainsi de suite jusqu’à un dernier exemplaire de 32 * 32 pixel. Chaque exemplaire correspond à un '''niveau de détail''', aussi appelé ''Level Of Detail'' (abrévié en LOD). La résolution utilisée diminue d'autant plus que l'objet est situé loin de la caméra. Les objets proches seront rendus avec la texture 512*512, ceux plus lointains seront rendus avec la texture de résolution 256*256, les textures 128*128 seront utilisées encore plus loin, et ainsi de suite jusqu'aux objets les plus lointains qui sont rendus avec la texture la plus petite de 32*32.
[[File:MipMap Example STS101.jpg|centre|vignette|upright=2|Exemples de mip-maps.]]
Le ''mip-mapping'' améliore grandement la qualité d'image. L'image d'exemple ci-dessous le montre assez bien.
[[File:Mipmapping example.png|centre|vignette|upright=2|Exemple de mipmapping.]]
Pour faciliter les calculs d'adresse, les LOD d'une même texture sont stockées les uns après les autres en mémoire (dans un tableau, comme diraient les programmeurs). Ainsi, pas besoin de se souvenir de la position en mémoire de chaque LOD : l'adresse de la texture de base, et quelques astuces arithmétiques suffisent. Prenons le cas où la texture de base a une taille L. le premier exemplaire est à l'adresse 0, le second niveau de détail est à l'adresse L, le troisième à l'adresse L + L/4, le suivant à l'adresse L + L/4 + L/16, et ainsi de suite. Le calcul d'adresse demande juste connaître le niveau de détails souhaité et l'adresse de base de la texture. Le niveau de détail voulu est calculé par les pixel shaders, en fonction de la coordonnée de profondeur du pixel à traiter.
Évidemment, cette technique consomme de la mémoire vidéo, vu que chaque texture est dupliquée en plusieurs exemplaires, en plusieurs LOD. Dans le détail, la technique du mip-mapping prend au maximum 33% de mémoire en plus (sans compression). Cela vient du fait qu'en prenant une texture dexu fois plus petite, elle prend 4 fois moins de mémoire : 2 fois moins de pixels en largeur, et 2 fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des LODs, texture de base comprise, prendra X + (X/4) + (X/16) + (X/256) + … Un petit calcul de limite donne 4/3 * X, soit 33% de plus.
===Le cube-mapping===
[[File:Cube mapped reflection example 2.JPG|vignette|Exemple de reflets environnementaux.]]
L''''environnement-mapping''' est une technique de calcul de divers effets graphiques liés à l'environnement, notamment des réflexions. L'idée est de plaquer une texture pré-calculée pour simuler l'effet de l'environnement sur une surface ou un objet 3D. Il en existe plusieurs versions différentes, mais la seule utilisée de nos jours est le ''cube-mapping'', où la texture de l'environnement est plaquée sur un cube, d'où son nom. Le cube en question est utilisé différemment suivant ce que l'on cherche à faire avec le ''cube-mapping''. Les deux utilisations principales sont le rendu du ciel et des décors, et les réflexions sur la surface des objets. Dans les deux cas, l'idée est de précalculer ce que l'on voit du point de vue de la caméra. On place la caméra dans la scène 3D, on place un cube centré sur la caméra, le cube est texturé avec ce que l'on voit de l'environnement depuis la caméra/l'objet de son point de vue.
[[File:Panorama cube map.png|centre|vignette|upright=2|L'illustration montre en premier lieu une ''cubemap'' avec les six faces mises en évidence, puis quel environnement 3D elle permet de simuler, le troisième illustration montrant comment la ''cubemap'' est utilisée pour simuler l'environnement.]]
Le rendu du ciel et des décors lointains dans les jeux vidéo se base sur des '''''skybox''''', à savoir un cube centré sur la caméra, sur lequel on ajoute des textures de ciel ou de décors lointains. Le cube est recouvert par une texture, qui correspond à ce que l'on voit quand on dirige le regard de la caméra vers cette face. Contrairement à ce qu'on pourrait croire, la skybox n'est pas les limites de la scène 3D, les limites du niveau d'un jeu vidéo ou quoique ce soit d'autre de lié à la physique de la scène 3D. La skybox est centrée sur la caméra, elle suit la caméra dans son mouvement. Centrer la skybox sur la caméra permet de simuler des décors très lointains, suffisamment lointain pour qu'on n'ait pas l'illusion de s'en rapprocher en se déplaçant dans la map. De plus, cela évite d'avoir à faire trop de calculs à chaque fois que l'on bouge la caméra. La texture plaquée sur le cube est une texture unique, elle-même découpée en six sous-textures, une par face du cube.
[[File:Skybox example.png|centre|vignette|upright=2|Exemple de Skybox.]]
[[File:Cube mapped reflection example.jpg|vignette|Réflexions calculées par une ''cubemap''.]]
Le ''cube-mapping'' est aussi utilisé pour des reflets. L'idée est de simuler les reflets en plaquant une texture pré-calculée sur l'objet réflecteur. La texture pré-calculée est un dessin de l'environnement qui se reflète sur l'objet, un dessin du reflet à afficher. En la plaquant la texture sur l'objet, on simule ainsi des reflets de l'environnement, mais on ne peut pas calculer d'autres reflets comme les reflets objets mobiles comme les personnages. Et il se trouve que la texture pré-calculée est une ''cubemap''. Pour les environnements ouverts, c'est la ''skybox'' qui est utilisée, ce qui permet de simuler les reflets dans les flaques d'eau ou dans des lacs/océans/autres. Pour les environnements intérieurs, c'est une cubemap spécifique qui utilisée. Par exemple, pour l'intérieur d'une maison, on a une ''cubemap'' par pièce de la maison. Les reflets se calculent en précisant quelle ''cubemap'' appliquer sur l'objet en fonction de la direction du regard.
[[File:Cube map level.png|centre|vignette|Cube map de l'intérieur d'une pièce d'un niveau de jeux vidéo.]]
Toujours est-il que les textures utilisées pour le ''cubemmapping'', appelées des ''cubemaps'', sont en réalité la concaténation de six textures différentes. En mémoire vidéo, la ''cubemap'' est stockée comme six textures les unes à la suite des autres. Lors du rendu, on doit préciser quelle face du cube utiliser, ce qui fait 6 possibilités. On a le même problème qu'avec les niveaux de détail, sauf que ce sont les faces d'une ''cubemap'' qui remplacent les textures de niveaux de détails. L'accès en mémoire doit donc préciser quelle portion de la ''cubemap'' il faut accéder. Et l'accès mémoire se complexifie donc. Surtout que l'accès en question varie beaucoup suivant l'API graphique utilisée, et donc suivant la carte graphique.
Les API 3D assez anciennes ne gérent pas nativement les ''cubemaps'', qui doivent être émulées en logiciel en utilisant six textures différentes. Le pixel shader décide donc quelle ''cubemap'' utiliser, avec quelques calculs sur la direction du regard. L'accès se fait d'une manière assez simple : le shader choisit quelle texture utiliser. Les API 3D récentes gèrent nativement les ''cubemaps''. Dans le cas le plus simple,pour les versions les plus vielles de ces API, les six faces sont numérotées et l'accès à une ''cubemap'' précise quel face utiliser en donnant son numéro. La carte graphique choisit alors automatiquement la bonne texture, mais cela demande de laisser le calcul de la bonne face au pixel shader. D'autres API 3D et cartes graphiques font autrement. Dans les API 3D modenres, les ''cubemap'' sont gérées comme des textures en trois dimensions, adressées avec trois coordonnées u,v,w. La carte graphique utilise ces trois coordonnées de manière à en déduire quelle est la face pertinente, mais aussi les coordonnées u,v dans la texture de la face.
==L'implémentation matérielle du placage de textures==
Pour résumer, la lecture d'un texel demande d'effectuer plusieurs étapes. Dans le cas le plus simple, sans ''mip-mapping'' ou ''cubemapping'', on doit effectuer les étapes suivantes :
* Il faut d'abord normaliser les coordonnées de texture pour qu'elles tombent dans l'intervalle [0,1] en fonction du mode d'adressage désiré.
* Ensuite, les coordonnées u,v doivent être converties en coordonnées entières, ce qui demande une multiplication flottante.
* Enfin, l'adresse finale est calculée à partir des coordonnées entières et en ajoutant l'adresse de base de la texture (et éventuellement avec d'autres calculs arithmétiques suivant le format de la texture).
Tout cela pourrait être fait par le pixel shaders, mais cela implique beaucoup de calculs répétitifs et d'opérations arithmétiques assez lourdes, avec des multiplications flottantes, des additions et des multiplications entières, etc. Faire faire tous ces calculs par les shaders serait couteux en performance, sans compter que les shaders deviendraient plus gros et que cela aurait des conséquences sur le cache d'instruction. De plus, certaines de ces étapes peuvent se faire en parallèle, comme les deux premières, ce qui colle mal avec l'aspect sériel des shaders.
Aussi, les processeurs de shaders incorporent une unité de calcul d'adresse spéciale pour faire ces calculs directement en matériel. L'unité de texture contient au minimum deux circuits : un circuit de calcul d'adresse, et un circuit d'accès à la mémoire. Toute la difficulté tient dans le calcul d'adresse, plus que dans le circuit de lecture. Le calcul d'adresse est conceptuellement réalisé en deux étapes. La première étape qui transforme les coordonnées u,v en coordonnées x,y qui donne le numéro de la ligne et de la colonne du texel dans la texture. La seconde étape prend ces deux coordonnées x,y, l'adresse de la texture, et détermine l'adresse de la tile à lire.
[[File:Unité de texture simple.png|centre|vignette|upright=2|Unité de texture simple]]
===L'implémentation du mip-mapping===
Le ''mip-mapping'' est lui aussi pris en charge par l'unité de calcul d'adresse, car cette technique change l'adresse de base de la texture. La gestion du ''mip-mapping'' est cependant assez complexe. Il est possible de laisser le pixel shader calculer quel niveau de détail utiliser, en fonction de la coordonnée de profondeur z du pixel à afficher. La carte graphique détermine alors automatiquement quelle texture lire, quel niveau de détail, automatiquement. Elle détermine aussi la bonne résolution pour la texture, qui est égal à la résolution de la texture de base, divisée par le niveau de détail. Pour résumer, le niveau de détail est envoyé aux unités de texture, qui s'occupent de calculer l'adresse de base et la résolution adéquates. Quelques calculs arithmétiques simples, donc, qui s'implémentent facilement avec quelques circuits.
Mais une autre méthode laisse la carte graphique déterminer le niveau de détail par elle-même. Dans ce cas, cela demande, outre les deux coordonnées de texture, de calculer la dérivée de ces deux coordonnées dans le sens horizontal et vertical, ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Les quatre dérivées sont les suivantes :
: <math>\frac{du}{dx}</math>, <math>\frac{dv}{dx}</math>, <math>\frac{du}{dy}</math>, <math>\frac{dv}{dy}</math>
Un bon moyen pour obtenir les dérivées demande de regrouper les pixels par groupes de 4 et de faire la différence entre leurs coordonnées de texture respectives. On peut calculer les deux dérivées horizontales en comparant les deux pixels sur la même ligne, et les deux dérivées verticales en comparant les deux pixels sur la même colonne. Mais cela demande de rastériser les pixels par groupes de 4, par ''quads''. Et c'est ce qui est fait sur les cartes graphiques actuelles, qui rastérisent des groupes de 4 pixels à la fois.
[[File:Texture sampler unit with mipmapping.png|centre|vignette|upright=2.0|Unité de texture avec mipmapping.]]
Malheureusement, le calcul exact utilisé pour le choix de la mip-map dépend du GPU considéré et peu de chose est connu quant à ces algorithmes. Il est possible d'inférer le comportement à partir d'observations, mais guère plus. Pour ceux qui veulent en savoir plus, je conseille la lecture de cet article de blog :
* [https://pema.dev/2025/05/09/mipmaps-too-much-detail/ Mipmap selection in too much detail]
===La gestion des accès mémoire===
Enfin, l'unité de texture doit tenir compte du fait que la mémoire vidéo met du temps à lire une texture. En théorie, l'unité de texture ne devrait pas accepter de nouvelle demande de lecture tant que celle en cours n'est pas terminée. Mais faire ainsi demanderait de bloquer tout le pipeline, de l'''input assembler'' au unités de''shaders'', ce qui est tout sauf pratique et nuirait grandement aux performances.
Une solution alternative consiste à mettre en attente les demandes de lectures de texture pendant que la mémoire est occupée. La manière la plus simple d'implémenter des accès mémoire multiples est de les mettre en attente dans une petite mémoire FIFO. Cela implique que les accès mémoire s’exécutent dans l'ordre demandé par le ''shader'' et/ou l'unité de rastérisation, il n'y a pas de réorganisation des accès mémoire ou d’exécution dans le désordre des accès mémoire.
[[File:Texture prefetching.png|centre|vignette|upright=1.5|Accès mémoire simultanés.]]
Évidemment, quand la mémoire FIFO est pleine, le pipeline est alors totalement bloqué. Le rasteriser est prévenu que l'unité de texture ne peut pas accepter de nouvelle lecture de texture. En pratique, la FIFO est généralement d'une taille respectable et permet de mettre en attente beaucoup de demandes de lecture de texture. Il faut de plus noter qu'il y a une FIFO par processeur de ''shader'' sur les cartes graphiques modernes. Quand elle est pleine, le processeur cesse d'exécuter de nouveaux accès mémoire, mais peut continuer à exécuter des ''shaders'' dans les autres unités de calcul, pas besoin de bloquer complétement le pipeline.
===L'intégration du cache de textures===
Il faut noter que les unités de texture incorporent aussi un cache de texture, voire plusieurs. L'intégration des caches de texture avec la mémoire FIFO précédente est quelque peu compliqué, car il faut garantir que les lectures de texture se fassent dans le bon ordre. On ne peut pas exécuter une lecture dans le cache alors que des lectures précédentes sont en attente de lecture en mémoire vidéo. Et cela pose un gros problème : une lecture dans le cache de texture prend quelques dizaines de cycles d'horloge, alors qu'une lecture en mémoire vidéo en prend facilement 400 à 800 cycles, parfois plus. Et cela fait que l'ordre des accès mémoire peut s'inverser.
Prenons par exemple un accès au cache précédé et suivi par deux accès en mémoire vidéo. Le premier démarre au cycle 1, et se termine au cycle numéro 400. L'accès au cache commence au cycle 2 et se termine 20 cycles après, au cycle numéro 22. En clair, la lecture dans le cache s'est terminée avant l'accès mémoire qui le précède. Les textures ne sont donc plus lues dans l'ordre. Et il faut trouver une solution pour éviter cela.
La solution est de retarder les lectures dans le cache tant que tous les accès précédents ne sont pas terminés. Mais pour retarder les lectures en question, il faut d'abord savoir si la lecture atterrit dans le cache ou non, ce qui demande d'accéder au cache. On fait face à un dilemme : on veut retarder les accès au cache, mais les différencier des lectures déclenchant des accès mémoire demande d'accéder au cache en premier lieu. La solution est décrite dans l'article "Prefetching in a Texture Cache Architecture" par Igehy et ses collègues. Elle se base sur deux idées combinées ensemble.
La première idée est de séparer l'accès au cache en deux : une étape qui vérifie si les texels à lire sont dans le cache, et une étape qui accède aux données dans le cache lui-même. Un cache de texture est donc composé de deux circuits principaux. Le premier vérifie la présence des texels dans le cache. Il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Ensuite, en plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
La seconde idée est de retarder l'accès au cache entre les deux phases. La première étape d'un accès mémoire vérifie si la donnée est dans le cache ou non. Puis, on retarde la lecture des données, pour attendre que toutes les lectures précédentes soient terminées. Et enfin, troisième étape : la lecture des texels dans la mémoire cache proprement dite. Les accès mémoire passant par la mémoire vidéo se font de la même manière, à une différence près : la lecture dans le cache est remplacée par la lecture en mémoire vidéo. Tout démarre avec une demande à l'unité de tags, qui vérifie si le texel est dans le cache ou non. Puis on retarde l'accès tant que la mémoire vidéo est occupée, puis on effectue la lecture en mémoire vidéo.
Si ce n'est pas le cas, l'accès mémoire est envoyé à la mémoire vidéo comme précédemment, à savoir qu'il est mis en attente dans une mémoire FIFO, puis envoyé à la mémoire vidéo dès que celle-ci est libre. Mais en sortie de la mémoire, la donnée lue est envoyée dans le cache de texture, par dans l'unité de filtrage. Pour savoir où placer la donnée lue, l'unité de tag a réservé une ligne de cache précise, une adresse bien précise. L'adresse en question est disponible en lisant une autre mémoire FIFO, qui a mis en attente l'adresse en question, en attendant que l'accès mémoire se termine. La donnée est alors écrite dans le cache, puis lue par l'unité de filtrage de textures.
Pour une lecture dans le cache, le déroulement est similaire, mais sans le passage par la mémoire. La lecture fait une demande à l'unité de tag, et celle-ci répond que la donnée est bien dans le cache. Elle place alors l'adresse à lire dans la file d'attente. Une fois que les accès mémoire précédents sont terminés, l'adresse sort de la file d'attente et est envoyée à la mémoire de données. La lecture s'effectue, les texels sont envoyés à l'unité de filtrage de textures. La seule différence avec un ''phased cache'' normal est l'insertion de l'adresse à lire dans une FIFO qui vise à mettre en attente
[[File:Unité de texture avec un cache de texture.png|centre|vignette|upright=2.0|Unité de texture avec un cache de texture]]
Pour résumer, l'implémentation précédente garantit une exécution des lectures dans leur ordre d'arrivée. Et pour cela, elle retarde les lectures dans le cache tant que les lectures en mémoire précédentes ne sont pas terminées. L'accès au cache est plus rapide que l'accès en mémoire vidéo, mais le retard ajouté pour garantir l'ordre des lectures fait que le temps d'accès est très long.
==Le filtrage de textures==
Plaquer des textures sans autre forme de procès ne suffit pas à garantir des graphismes d'une qualité époustouflante. La raison est que les sommets et les texels ne tombent pas tout pile sur un pixel de l'écran : le sommet associé au texel peut être un petit peu trop en haut, ou trop à gauche, etc. Une explication plus concrète fait intervenir les coordonnées de texture. Souvenez-vous que lorsque l'on traduit une coordonnée de texture u,v en coordonnées x,y, on obtient un résultat qui ne tombe pas forcément juste. Souvent, le résultat a une partie fractionnaire. Si celle-ci est non-nulle, cela signifie que le texel/sommet n'est pas situé exactement sur le pixel voulu et que celui-ci est situé à une certaine distance. Concrètement, le pixel tombe entre quatre texels, comme indiqué ci-dessous.
[[File:Filtrage texture.png|centre|vignette|upright=2.0|Position du pixel par rapport aux texels.]]
Pour résoudre ce problème, on doit utiliser différentes techniques d'interpolation, aussi appelées techniques de '''filtrage de texture''', qui visent à calculer la couleur du pixel final en fonction des texels qui l'entourent. Il existe de nombreux types de filtrage de textures, qu'il s'agisse du filtrage linéaire, bilinéaire, trilinéaire, anisotropique et bien d'autres.
Tous ont besoin d'avoir certaines informations qui sont généralement fournies par les circuits de calcul d'adresse. La première est clairement la partie fractionnaire des coordonnées x,y. La seconde est la dérivée de ces deux coordonnées dans le sens horizontal et vertical., ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Toujours est-il que le filtrage de texture est une opération assez lourde, qui demande beaucoup de calculs arithmétiques. On pourrait en théorie le faire dans les pixels shaders, mais le cout en performance serait absolument insoutenable. Aussi, les cartes graphiques intègrent toutes un circuit dédié au filtrage de texture, le ''texture sampler''. Même les plus anciennes cartes graphiques incorporent une unité de filtrage de texture, ce qui nous montre à quel point cette opération est importante.
[[File:Texture unit.png|centre|vignette|upright=2.0|Unité de texture.]]
On peut configurer la carte graphique de manière à ce qu'elle fasse soit du filtrage bilinéaire, soit du filtrage trilinéaire, on peut configurer le niveau de filtrage anisotropique, etc. Cela peut se faire dans les options de la carte graphique, mais cela peut aussi être géré par l'application. La majorité des jeux vidéos permettent de régler cela dans les options. Ces réglages ne concernent pas la texture elle-même, mais plutôt la manière dont l'unité de texture doit fonctionner. Ces réglages sur l''''état de l'unité de texture''' sont mémorisés quelque part, soit dans l'unité de texture elle-même, soit fournies avec la ressource de texture elle-même, tout dépend de la carte graphique. Certaines cartes graphiques mémorisent ces réglages dans les unités de texture ou dans le processeur de commande, et tout changement demande alors de réinitialiser l'état des unités de texture, ce qui prend un peu de temps. D'autres placent ces réglages dans les ressources de texture elles-mêmes, ce qui rend les modifications de configuration plus rapides, mais demande plus de circuits. D'autres cartes graphiques mélangent les deux options, certains réglages étant globaux, d'autres transmis avec la texture. Bref, difficile de faire des généralités, tout dépend du matériel et le pilote de la carte graphique cache tout cela sous le tapis.
Maintenant que cela est dit, voyons quelles sont les différentes méthodes de filtrage de texture et comment la carte graphique fait pour les calculer.
===Le filtrage au plus proche===
La méthode de filtrage la plus simple consiste à colorier avec le texel le plus proche. Cela revient tout simplement à ne pas tenir compte de la partie fractionnaire des coordonnées x,y, ce qui est très simple à implémenter en matériel. C'est ce que l'on appelle le '''filtrage au plus proche''', aussi appelé ''nearest filtering''.
Autant être franc, le résultat est assez pixelisé et peu agréable à l’œil. Par contre, le résultat est très rapide à calculer, vu qu'il ne demande aucun calcul à proprement parler. Elle ne fait pas appel à la parti fractionnaire des coordonnées entières de texture, ni aux dérivées de ces coordonnées. On peut combiner cette technique avec le mip-mapping, ce qui donne un résultat bien meilleur, bien que loin d'être satisfaisant. Au passage, toutes les techniques de filtrage de texture peuvent se combiner avec du mip-mapping, certaines ne pouvant pas faire sans.
[[File:Interpolation-nearest.svg|centre|vignette|Filtrage de texture au plus proche.]]
===Le filtrage linéaire===
Le filtrage le plus simple est le '''filtrage linéaire'''. Il effectue une interpolation linéaire entre deux mip-maps, deux niveaux de détails. Pour comprendre l'idée, nous allons prendre une situation très simple, avec une texture carrée de 512 texels de côté. Le mip-mapping crée plusieurs textures : une de 256 texels de côté, une de 128 texels, une de 64, etc. Maintenant, la texture est sur un objet à une certaine distance de l'écran, vu de face. Le résultat est qu'elle correspond à l'écran à un carré de 300 pixels de côté (pas d'erreur : pixels, pas texels). Dans ce cas, la texture se trouve entre deux mip-maps : celle de 512 pixels de côté, celle de 256. Laquelle choisir ? Le filtrage au plus proche prend la texture de 512 pixels de côté. Le filtrage linéaire lui, fait autrement.
Vu que la texture est entre deux mip-maps, l'idée est de prendre le texel au plus proche dans chaque texture et de faire une sorte de moyenne appelée l'interpolation linéaire. L'interpolation par du principe que la couleur varie entre les deux texels en suivant une fonction affine, illustrée ci-dessous. Ce ne serait évidemment pas le cas dans le monde réel, mais on supposer cela donne une bonne approximation de ce à quoi ressemblerait une texture à plus haute résolution. On peut alors calculer la couleur du pixel par une simple moyenne pondérée par la distance. Le résultat est que les transitions entre deux niveaux de détails sont plus lisses, moins abruptes.
[[File:Lin interp -é.png|centre|vignette|upright=2.0|Interpolation linéaire.]]
===Le filtrage bilinéaire===
Le filtrage bilinéaire effectue une sorte de moyenne pondérée des quatre texels les plus proches du pixel à afficher. Pour cela, rappelez-vous ce qui a été dit plus haut : les coordonnées x,y d'un pixel ont une partie entière et une partie fractionnaire. Le filtrage au plus proche élimine les parties fractionnaires, ce qui donne une coordonnée x,y. Avec le filtrage bilinéaire, on prend les texels de coordonnées (x,y) ; (x+1,y) ; (x,y+1) ; (x+1,y+1), le pixel étant entre ces 4 texels.
Mais le filtrage ne fait pas qu'une simple moyenne, il prend en compte les parties fractionnaires pour faire la moyenne. En effet, le pixel n'est pas au milieu du carré de texel, il est quelque part mais est souvent plus proche d'un texel que des autres. Et il faut donc pondérer la moyenne par les distances aux 4 texels. Pour cela, la moyenne est calculée à partir d'interpolations linéaires. Avec 4 pixels, nous allons devoir calculer la couleur de deux points intermédiaires. La couleur de ces deux points se calcule par interpolation linéaire, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.
[[File:Bilin3.png|centre|vignette|upright=2|Filtrage bilinéaire de texture.]]
Le circuit qui permet de faire l'interpolation bilinéaire est particulièrement simple. On trouve un circuit de chaque pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu, et un pour la transparence. Chacun de ces circuit est composé de sous-circuits chargés d'effectuer une interpolation linéaire, reliés comme suit.
[[File:Texture sampler unit.png|centre|vignette|Unité de filtrage bilinéaire.]]
Vous noterez que le filtrage bilinéaire accède à 4 pixels en même temps. Fort heureusement, les textures sont stockées de manière à ce qu'on puisse charger les 4 pixels en une fois, comme on l'a vu plus haut. Le filtrage bilinéaire a de fortes chances que les 4 pixels filtrés soient dans la même ''tile'', la seule exception étant quand ils sont tout juste sur le bord d'une ''tile''.
: La console de jeu Nintendo 64 n'utilise que trois pixels au lieu de quatre dans son interpolation bilinéaire, qui en devient une interpolation quasi-bilinéaire. La raison derrière ce choix est une question de performances, comme beaucoup de décisions de ce genre. Le résultat est un rendu imparfait de certaines textures.
===Le filtrage trilinéaire===
Avec le filtrage bilinéaire, des discontinuités apparaissent sur certaines surfaces. Par exemple, pensez à une texture de sol : elle est appliquée plusieurs fois sur toute la surface du sol. A une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512*512 à 256*256, ce qui est visible pour un joueur attentif. De telles transitions sont lissées grâce au filtrage linéaire, il n'y a plus qu'à le combiner avec le filtrage bilinéaire. Rien d’incompatible : le premier filtre l'intérieur d'une mip-map, le second combine deux mip-maps.
Le filtrage trilinéaire prend les deux mip-maps les plus proches, fait un filtrage bilinéaire avec chacune, puis fait une « une moyenne » pondérée entre les deux résultats. Le circuit de filtrage trilinéaire existe en plusieurs versions. La plus simple, illustrée ci-dessous, effectue deux filtrages bilinéaires en parallèle, dans deux circuits séparés, puis combine leurs résultats avec un circuit d'interpolation linéaire. Mais ce circuit nécessite de charger 8 texels simultanément. Qui plus est, ces 8 texels ne sont pas consécutifs en mémoire, car ils sont dans deux niveaux de détails/mip-maps différents.
[[File:Parallel trilinear filtering.png|centre|vignette|upright=2.0|Unité de filtrage trilinéaire parallèle.]]
Vu qu'on lit des texels dans deux mip-maps, les texels sont lus en deux fois : 4 texels provenant de la première mip-map, suivis par les 4 texels de l'autre mip-map. Les 4 premiers texels doivent donc être mis en attente dans des registres, en attendant que les 4 autres arrivent. Une amélioration du circuit précédent gère cela en ajoutant des registres. Il lit les 4 premiers texels, les filtre avec une interpolation bilinéaire, et mémorise le résultat dans un registre. Puis, il lit les 4 autres texels, les filtre, et met le résultat dans un second registre. A ce moment là, un circuit d'interpolation linéaire finit le travail. On économise donc un circuit d'interpolation bilinéaire, sans que les performances soient trop impactées.
[[File:Filtrage trilineaire.png|centre|vignette|upright=1.0|Unité de filtrage trilineaire série.]]
Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le temps où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. À chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients, et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.
===Le filtrage anisotrope===
D'autres artefacts peuvent survenir lors de l'application d'une texture, la perspective pouvant déformer les textures et entraîner l'apparition de flou. La raison à cela est que les techniques de filtrage de texture précédentes partent du principe que la texture est vue de face. Prenez une texture carrée, par exemple. Vue de face, elle ressemble à un carré sur l'écran. Mais tournez la caméra, de manière à voir la texture de biais, avec un angle, et vous verrez que la forme de la texture sur l'écran est un trapèze, pas un carré. Cette déformation liée à la perspective n'est pas prise en compte par les méthodes de filtrage de texture précédentes. Pour le dire autrement, les techniques de filtrage précédentes partent du principe que les 4 texels qui entourent un pixel forment un carré, ce qui est vrai si la texture est vue de face, sans angle, mais ne l'est pas si la texture n'est pas perpendiculaire à l'axe de la caméra. Du point de vue de la caméra, les 4 texels forment un trapèze d'autant moins proche d'un carré que l'angle est grand.
Pour corriger cela, les chercheurs ont inventé le '''filtrage anisotrope'''. En fait, je devrais plutôt dire : LES filtrages anisotropes. Il en existe un grand nombre, dont certains ne sont pas utilisés dans les cartes graphiques actuelles, soit car ils trop gourmand en accès mémoires et en calculs pour être efficaces, soit car ils ne sont pas pratiques à mettre en œuvre. Il est très difficile de savoir quelles sont les techniques de filtrage de texture utilisées par les cartes graphiques, qu'elles soient récentes ou anciennes. Beaucoup de ces technologies sont brevetées ou gardées secrètes, et il faudrait vraiment creuser les brevets déposés par les fabricants de GPU pour en savoir plus. Les algorithmes en question seraient de plus difficiles à comprendre, les méthodes mathématiques cachées derrière ces méthodes de filtrage n'étant pas des plus simple.
[[File:Anisotropic filtering en.png|centre|vignette|upright=2|Exemple de filtrage anisotrope.]]
==La compression de textures==
Les textures les plus grosses peuvent aller jusqu'au mébioctet, ce qui est beaucoup. Pour limiter la casse, les textures sont compressées. La '''compression de texture''' réduit la taille des textures, ce qui peut se faire avec ou sans perte de qualité. Elle entraîne souvent une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande. Mais il s'agit là d'une technique très simple, beaucoup plus simple que les techniques que nous allons voir dans cette section. Nous allons voir quelque algorithmes de compression de textures de complexité intermédiaire, mais n'allons pas voir l'état de l'art. Il existe des formats de texture plus récents que ceux qui nous allons aborder, comme l{{'}}''Ericsson Texture Compression'' ou l{{'}}''Adaptive Scalable Texture Compression'', plus complexes et plus efficaces.
Notons que les textures sont compressées dans les fichiers du jeu, mais aussi en mémoire vidéo. Les textures sont décompressées lors de la lecture. Pour cela, la carte graphique contient alors un circuit, capable de décompresser les textures lorsqu'on les lit en mémoire vidéo. Les cartes graphiques supportent un grand nombre de formats de textures, au niveau du circuit de décompression. Du fait que les textures sont décompressées à la volée, les techniques de compression utilisées sont assez particulières. La carte graphique ne peut pas décompresser une texture entière avant de pouvoir l'utiliser dans un ''pixel shader''. A la place, on doit pouvoir lire un morceau de texture, et le décompresser à la volée. On ne peut utiliser les méthodes de compression du JPEG, ou d'autres formats de compression d'image. Ces dernières ne permettent pas de décompresser une image morceau par morceau.
Pour permettre une décompression/compression à la volée, les textures sont des textures tilées, généralement découpées en tiles de 4 * 4 texels. Les ''tiles'' sont compressées indépendamment les unes des autres. Et surtout, avec ou sans compression, la position des tiles en mémoire ne change pas. On trouve toujours une tile tous les T octets, peu importe que la tile soit compressée ou non. Par contre, une tile compressée n'occupera pas T octets, mais moins, là où une tile compressée occupera la totalité des T octets. En clair, compresser une tile fait qu'il y a des vides entre deux tiles dans al mémoire vidéo, mais ne change rien à leur place en mémoire vidéo qui est prédéterminée, peu importe que la texture soit compressée ou non. L'intérêt de la compression de textures n'est pas de réduire la taille de la texture en mémoire vidéo, mais de réduire la quantité de données à lire/écrire en mémoire vidéo. Au lieu de lire T octets pour une tile non-compressée, on pourra en lire moins.
===La palette indicée et la technique de ''Vector quantization''===
La technique de compression des textures la plus simple est celle de la '''palette indicée''', que l'on a entraperçue dans le chapitre sur les cartes d'affichage. La technique de '''''vector quantization''''' peut être vue comme une amélioration de la palette, qui travaille non pas sur des texels, mais sur des ''tiles''. À l'intérieur de la carte graphique, on trouve une table qui stocke toutes les ''tiles'' possibles. Chaque ''tile'' se voit attribuer un numéro, et la texture sera composé d'une suite de ces numéros. Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l’embarqué utilisent ce genre de compression.
===Les algorithmes de ''Block Truncation coding''===
La première technique de compression élaborée est celle du '''''Block Truncation Coding''''', qui ne marche que pour les images en niveaux de gris. Le BTC ne mémorise que deux niveaux de gris par ''tile'', que nous appellerons couleur 1 et couleur 2, les deux niveaux de gris n'étant pas le même d'une ''tile'' à l'autre. Chaque pixel d'une ''tile'' est obligatoirement colorié avec un de ces niveaux de gris. Pour chaque pixel d'une ''tile'', on mémorise sa couleur avec un bit : 0 pour couleur 1, et 1 pour couleur 2. Chaque ''tile'' est donc codée par deux entiers, qui codent chacun un niveau de gris, et une suite de bits pour les pixels proprement dit. Le circuit de décompression est alors vraiment très simple, comme illustré ci-dessous.
[[File:Block Truncation coding.jpg|centre|vignette|upright=2.0|Block Truncation coding.]]
La technique du BTC peut être appliquée non pas du des niveaux de gris, mais pour chaque composante Rouge, Vert et Bleu. Dans ces conditions, chaque ''tile'' est séparée en trois sous-''tiles'' : un sous-bloc pour la composante verte, un autre pour le rouge, et un dernier pour le bleu. Cela prend donc trois fois plus de place en mémoire que le BTC pur, mais cela permet de gérer les images couleur.
===Le format de compression S3TC / DXTC===
L'algorithme de '''Color Cell Compression''', ou CCC, améliore le BTC pour qu'il gère des couleurs autre que des niveaux de gris. Ce CCC remplace les deux niveaux de gris par deux couleurs. Une ''tile'' est donc codée avec un entier 32 bits par couleur, et une suite de bits pour les pixels. Le circuit de décompression est identique à celui utilisé pour le BTC.
[[File:Color Cell Compression.jpg|centre|vignette|Color Cell Compression.]]
[[File:Dxt1-memory-layout.png|vignette|Dxt1 et ''color cell compression''.]]
Le format de compression de texture utilisé de base par Direct X, le DXTC, est une version amliorée de l'algorithme précédent. Il est décliné en plusieurs versions : DXTC1, DXTC2, etc. La première version du DXTC est une sorte d'amélioration du CCC : il ajoute une gestion minimale de transparence, et découpe la texture à compresser en ''tiles'' de 4 pixels de côté. La différence, c'est que la couleur finale d'un texel est un mélange des deux couleurs attribuée au bloc. Pour indiquer comment faire ce mélange, on trouve deux bits de contrôle par texel.
Si jamais la couleur 1 < couleur2, ces deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (2 * Couleur1 + Couleur2) / 3
* 11 = (Couleur1 + 2 * Couleur2) / 3
Sinon, les deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (Couleur1 + Couleur2) / 2
* 11 = Transparent
[[File:DXTC.jpg|centre|vignette|DXTC.]]
Le circuit de décompression du DXTC ressemble alors à ceci :
[[File:Circuit de décompression du DXTC.jpg|centre|vignette|upright=2.0|Circuit de décompression du DXTC.]]
===Les format DXTC 2, 3, 4 et 5 : l'ajout de la transparence===
Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3, lui-même replacé par le DXT4 et par le DXT5. Dans le DXT3, la transparence fait son apparition. Pour cela, on ajoute 64 bits par ''tile'' pour stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1.
[[File:Dxt23-memory-layout.png|centre|vignette|Dxt 2 et 3.]]
Dans le DXT4 et le DXT5, la méthode utilisée pour compresser les couleurs l'est aussi pour les valeurs de transparence. L'information de transparence est stockée par un en-tête contenant deux valeurs de transparence, le tout suivi d'une matrice qui attribue trois bits à chaque texel. En fonction de la valeur des trois bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1.
[[File:Dxt45-memory-layout.png|centre|vignette|Dxt 4 et 5.]]
===Le format de compression PVRTC===
Passons maintenant à un format de compression de texture un peu moins connu, mais pourtant omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle travaille surtout dans les cartes graphiques embarquées. Ses cartes se trouvent notamment dans l'ipad, l'iPhone, et bien d'autres smartphones actuels.
Avec le PVRTC, les textures sont encore une fois découpées en ''tiles'' de 4 texels par 4, mais la ressemblance avec le DXTC s’arrête là. Chacque ''tile'' est codée avec :
* une couleur codée sur 16 bits ;
* une couleur codée sur 15 bits ;
* 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
* et un bit de modulation, qui permet de configurer l’interprétation des bits de mélange.
Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4*4.
==Annexe : le ''normal-mapping'' hardware==
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, parlons un peu du ''normal mapping'' et de son implémentation dans les unités de texture. Pour rappel, 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. Et pour comprendre quel est le rapport avec les textures, nous allons devoir faire quelques rappels sur l'éclairage par pixel.
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
L'éclairage se fait en utilisant de nombreux calculs, qu'on a détaillé dans le chapitre sur les bases du rendu 3D. Ceux-ci utilisent la normale d'un sommet, à savoir un vecteur orienté à la verticale de la surface. En théorie, il y a une normale par sommet, ce qui fait que les calculs d'éclairage doivent se faire au niveau géométrique, avant la rastérisation. Mais pour obtenir un éclairage de meilleure qualité, il y a des techniques qui calcule l'éclairage d'une scène 3D pixel par pixel.
Une première technique d'éclairage par pixel interpole les normales lors de l'étape de rastérisation. On obtient alors un éclairage de Phong. Une autre solution est celle du ''normal mapping''. Le '''''normal mapping''''' précalcule les normales d'une surface dans une texture, appelée la ''normal map''. L'éclairage est alors réalisé par un pixel shader, qui lit les normales depuis cette texture et fait les calculs d'éclairage avec.
Notons que l'accès à la ''normal map'' se fait comme pour n'importe quelle texture : les texels sont lus en mémoire vidéo, puis le filtrage de texture est appliqué, et enfin le tout est envoyé au ''pixel shader''. En théorie, le filtrage effectue une sorte d'interpolation des normales automatique, ce qui permet de trouver la normale à un pixel précis, même s'il n'est pas sur un texel.
La ''normal map'' est une texture, donc. Mais ce n'est pas une texture comme une autre. Elle mémorise un vecteur pour chaque texel, pas une couleur. Il est possible d'utiliser les formats de textures vus plus haut, même si cela ne donne pas de bons résultats. Après tout, un vecteur est codé par trois coordonnées x,y,z, il est possible de coder chacune avec un octet et de packager cela dans une texture RGB classique. La coordonnée x va dans la composante Rouge, la coordonnée Y dans la composante Bleu, et la coordonnée z dans la composante z.
Le problème est que le rendu final n'est pas très beau. Et les opérations de filtrage ne donnent pas de très bons résultats. Pour remédier à ces problèmes, des formats de texture spécialisés pour les ''normal map'' ont été inventés. Le premier d'entre eux est le '''3Dc''', aussi appelé BC5 pour ''Block Compression 5''. Il utilise un octet par texel, au lieu de trois avec une texture RGB normale non-compressée.
===Le format de texture 3Dc===
Avec le 3Dc, les trois coordonnées des vecteurs ne sont pas mémorisées, seules deux d'entre elles le sont. En effet, les normales sont des vecteurs ''normalisés'', c'est à dire que leur longueur vaut 1, par construction. Vu que la taille est connue à l'avance, on peut en déduire la coordonnée z à partir des coordonnées x et y, avec l'usage du théorème de Pythagore. Par définition, <math>z^2 = 1 - x^2 - y^2</math>. Le calcul peut être fait dans le ''pixel shader'' sans problème.
Pour le reste, la compression de la ''normal map'' ressemble un peu à celle des textures. La ''normal map'' est découpée en blocs de 4 par 4 texels de côté, chacune étant encodée en tenant compte de certaines redondances. Dans un bloc, les coordonnées x sont dans un certain intervalle, allant de <math>x_{min}</math> à <math>x_{max}</math>, idem pour la coordonnée y qui est dans l'intervalle <math>y_{min}</math>, <math>y_{max}</math>. L'intervalle <math>x_{min}</math> à <math>x_{max}</math> est coupé en 8 parts égales, ce qui fait 8 valeurs possibles dans cet intervalle, chacune correspondant à une valeur x possible pour une normale du bloc. La normale est donc encodée en précisant quelle valeur est la bonne, en utilisant un indice de 3 bits pour cela. La valeur exacte de la coordonnée x se calcule à partir de l'indice comme suit :
: <math>x = x_{min} + \left( \frac{x_{min} - x_{min}}{8} \right) \times \text{indice} </math>
Pour encoder un bloc de 4 pr 4 normales, il faut donc :
* Un octet pour chaque coordonnée <math>x_{min}</math>, <math>x_{max}</math>, <math>y_{min}</math>, <math>y_{max}</math>.
* 6 bits par normale : 3 pour encoder le décalage de la coordonnée x, 3 pour la coordonnée y.
Le tout permet d'encoder un bloc de 16 normales en seulement 128 bits, soit un octet par normale. Le ''pixel shader'' se débrouille pour décompresser un bloc. Il effectue notamment le calcul du dessus, pour retrouver la coordonnée x.
==Annexe : les ''shadowmap'' hardware==
Les anciens GPU, notamment la Geforce FX, avaient des fonctionnalités spécifiques pour le calcul des ombres. Dans la plupart des jeux vidéos de l'époque, et même de maintenant, les ombres sont calculées avec la technique des ''shadowmap''. L'idée est assez simple sur le principe : un pixel est dans l'ombre quand il est invisible depuis une source de lumière.
L'idée est que le rendu est réalisé en plusieurs passes, avec une passe par source de lumière et une passe finale pour calculer l'image finale. Nous allons expliquer la technique avec une seule source de lumière, et allons utiliser l'exemple de la scène ci-dessous.
[[File:7fin.png|centre|vignette|Scène 3D d'exemple.]]
===La technique du ''shadowmapping''===
[[File:2shadowmap.png|vignette|Résultat de la première passe : ''shadowmap''..]]
La première passe rend l'image depuis le point de vue de la source de lumière. Cette première passe ne rend pas les couleurs de la scène, elle ne s'intéresse qu'à la profondeur des pixels. Le résultat est que l'image ne rend que le tampon de profondeur. Celui-ci est ensuite réutilisé comme texture pour la passe suivante. La texture en question est appelée la '''''shadownmap'''''.
La perspective utilisée, ainsi que le ''view frustrum'', dépend de la source de lumière. Pour une source de lumière qui émet un cône de lumière, le ''view frustrum'' de l'image rendue doit contenir tout le cone de lumière, et doit coller le plus possible à celui-ci. Pour une source directionnelle, comme le soleil, une perspective orthographique est utilisée.
La seconde passe rend l'image du point de vue de la caméra, pour rendre l'image finale. Elle rend l'image finale, qui est composée de pixels, chacun ayant une position à l'écran x,y, et une profondeur z. Les coordonnées sont transformées pour obtenir la position de ce pixel depuis le point de vue de la caméra. Une simple multiplication de matrice suffit, rien de bien compliqué, un shader peut le faire.
[[File:5failed.png|vignette|Résultat du test des comparaisons.]]
Après cette étape, on a alors les coordonnées x,y,z de ce pixel du point de vue de la caméra, et la ''shadowmap''. Il est alors possible d'accéder à la ''shadowmap'' au même endroit, à la même place que le pixel testé, aux mêmes coordonnées x,y. Si la profondeur du pixel est supérieure à celle de la shadowmap au même endroit, alors le pixel est situé derrière la surface visible, donc est dans l'ombre. Sinon, il n'est pas dans l'ombre. Le même procédé est répété sur chaque pixel de l'écran.
===Les optimisations hardware du ''shadowmapping''===
La technique des ''shadowmap'' demande donc de calculer une texture ''shadowmap'', puis de lire celle-ci et de faire des comparaisons de profondeur. Les GPU comme la Geforce FX intégraient du matériel dans les unités de texture pour faciliter ce travail. Les unités de texture pouvaient lire les ''shadowmap'', et faire la comparaison de profondeur toutes seules, elles avaient des circuits pour. Il suffisait de leur fournir le pixel à tester, ses coordonnées x,y,z, et l'adresse de la ''shadowmap''. Les unités de texture renvoyaient alors un résultat valant 0 ou 1 : 1 si le pixel est dans l'ombre, 0 sinon.
Elles pouvaient même effectuer du filtrage de texture sur les ''shadowmap''. Mais le filtrage était différent de celui utilisé sur les autres textures : moyenner des valeurs de profondeur ne marche pas bien. Elles utilisaient des techniques de filtrage différentes : elles faisaient les tests de comparaison, puis faisaient la moyenne des résultats. Ainsi, pour du filtrage bilinéaire, elles lisaient 4 texels dans la ''shadowmap'', puis faisaient 4 tests de comparaison, et moyennaient les 4 résultats.
==Annexe : les textures virtuelles==
Les '''textures virtuelles''' sont une optimisation des textures normales, qui visent à accélérer le rendu de terrains de grande taille. Imaginez par exemple un monde assez ouvert, comme un environnement en forêt ou en montagne, avec une grande distance de visibilité. Avec de tels terrains, le "sol" est recouvert par une texture de sol unique qui recouvre tout le terrain. Elle ne se répète pas, est de très grande taille, et peut parfois recouvrir toute la map ! Mais il n'y a pas assez de mémoire vidéo pour mémoriser la texture toute entière. La seule solution est la suivante : une partie de la texture est placée en mémoire vidéo, le reste est soit placé en mémoire RAM ou sur le disque dur.
Pour cela, le moteur de jeu utilise une optimisation ingénieuse, basée sur une observation assez basique : une bonne partie de la texture est visible, mais le reste est caché par des arbres, des habitations ou d'autres obstacles. Une optimisation possible de ne garder en mémoire vidéo que les portions visibles de la texture, pas les portions cachées. Une autre optimisation mélange textures virtuelles et ''mip-mapping''. L'idée est que pour les portions lointaines d'une texture, la texture utilisée est une ''mip-map'' de basse résolution. L'idée est alors de ne charger que la ''mip-map'' adéquate, pas les autres niveaux de détail. En clair, la texture de base n'est pas chargée en mémoire vidéo, mais la ''mip-map'' basse résolution l'est.
===Une texture à deux niveaux===
L'implémentation des textures virtuelles découpe les méga-textures en ''tiles'', en morceaux rectangulaires de taille modeste. En clair, le terrain est découpé en morceau rectangulaires/carrés. Seules les tiles nécessaires sont chargées en mémoire vidéo, pas les autres. Par exemple, les ''tiles'' non-visibles ne sont pas placées en mémoire vidéo, seules les ''tiles'' visibles le sont. De même, il y a une ''tile'' par niveau de mip-map : seul la tile correspondant le niveau adéquat est en mémoire vidéo, les autres niveaux de détail ne sont pas chargés. On peut faire une analogie avec la mémoire virtuelle, où les données sont découpées en pages, qui sont chargées en mémoire RAM à la demande, suivant les besoins, les données pouvant être swappées sur le disque dur si elles sont peu utilisées. Sauf qu'ici, il s'agit de textures qui sont découpées en pages chargées à la demande en mémoire vidéo, depuis la RAM système.
Une texture virtuelle est en réalité un système à deux niveaux : une liste de ''tiles'' et les ''tiles'' elles-mêmes. La liste de ''tiles'' est appelée un '''atlas de texture''', c'est un peu l'équivalent de la ''tilemap'' pour le rendu 2D. Rendre une texture demande de calculer quelle ''tile'' contient le texel à afficher, consulter la ''tile'' en question, puis récupérer le texel adéquat dans cette ''tile''. La ''tile'' est donc une texture, mais la texture à charger est choisie parmi un ensemble, qui est ici l'atlas de texture.
===L'implémentation : logicielle versus matérielle===
Les textures virtuelles ont été utilisées pour la première fois par les jeux Rage 1 et 2 d'IdSoftware, et quelques jeux ultérieurs comme DOOM 2016. IdSoftware les appelait des '''''mega-textures'''''. L'optimisation permettait des gains en performance assez impressionnants. Le jeu Rage 1 utilisait une texture carrée unique de 128k pixels de côté pour rendre le terrain. En théorie, une telle texture devrait prendre 64 giga-octets, mais le jeu tournait correctement avec 512 méga-octets de RAM, poussivement avec seulement 256 méga-octets de RAM.
De nos jours, les textures virtuelles sont supportées par beaucoup de jeux vidéos, les moteurs les plus courants gèrent de telles textures de manière logicielles. Mais quelques GPU récents supportent les textures virtuelles. Sur les GPU récents, l'atlas de texture est géré nativement par le matériel. Le GPU choisit quelle ''tile'', quelle texture choisir pour rendre le texel adéquat. Pour cela, le GPU calcule quelle ''tile'' charger, consulte l'atlas de texture, et lit la texture de ''tile'' adéquate.
Mais l'implémentation sur les GPU récents a de nombreuses limitations. La limitation la plus importante est que la taille des textures virtuelles ne peut pas dépasser la taille d'une texture normale, soit 32768 pixels de côté pour une texture carrée environ sur les GPU de 2020. De plus, le chargement d'une ''tile'' est très lent. En clair, dès qu'on veut changer de niveau de mip-map pour une tile, ou dès qu'une tile devient visible, le chargement de la tile peut facilement prendre plusieurs centaines de millisecondes. Le filtrage de texture est très complexe avec des textures virtuelles, ce qui fait que le filtrage de texture virtuelle est souvent soumis à des limitations que les textures normales n'ont pas, notamment pour le filtrage anisotropique.
{{NavChapitre | book=Les cartes graphiques
| prev=Le rasterizeur
| prevText=Le rasterizeur
| next=Les Render Output Target
| nextText=Les Render Output Target
}}{{autocat}}
n8rd4wf7iimt3gppfeivvepxb91icfg
763409
763408
2026-04-10T17:09:25Z
Mewtow
31375
/* Annexe : le normal-mapping hardware */
763409
wikitext
text/x-wiki
[[File:Texture mapping.png|vignette|''Texture mapping'']]
Les '''textures''' sont des images que l'on va plaquer sur la surface d'un objet, du papier peint en quelque sorte. Les cartes graphiques supportent divers formats de textures, qui indiquent comment les pixels de l'image sont stockés en mémoire : RGB, RGBA, niveaux de gris, etc. Une texture est donc composée de "pixels", comme toute image numérique. Pour bien faire la différence entre les pixels d'une texture, et les pixels de l'écran, les pixels d'une texture sont couramment appelés des ''texels''.
==Le placage de textures inverse==
Pour rappel, plaquer une texture sur un objet consiste à attribuer un texel à chaque sommet, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture.
Dans les faits, on n'utilise pas de coordonnées entières de ce type. Les coordonnées de texture sont deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. Le nom donnée à cette technique de description des coordonnées de texture s'appelle l''''''UV Mapping'''''.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Les API 3D modernes gèrent des textures en trois dimensions, ce qui ajoute une troisième coordonnée de texture notée w. Dans ce qui va suivre, nous allons passer les textures en trois dimensions sous silence. Elles ne sont pas très utilisées, la quasi-totalité des jeux vidéo et applications 3D utilisant des textures en deux dimensions. Par contre, le matériel doit gérer les textures 3D, ce qui le rend plus complexe que prévu. Il faut ajouter quelques circuits pour, de quoi gérer la troisième coordonnée de texture, etc.
Lors de la rastérisation, chaque fragment se voit attribuer un sommet, et donc la coordonnée de texture qui va avec. Si un pixel est situé pile sur un sommet, la coordonnée de texture de ce sommet est attribuée au pixel. Si ce n'est pas le cas, la coordonnée de texture finale est interpolée à partir des coordonnées des trois sommets du triangle rastérisé. L'interpolation en question a lieu dans l'étape de rastérisation, comme nous l'avons vu dans le chapitre précédent. Le fait qu'il y ait une interpolation fait que les coordonnées du pixel gagent à être des nombres flottants. On pourrait faire une interpolation avec des coordonnées de texture entières, mais les arrondis et autres imprécisions de calcul donneraient un résultat graphiquement pas terrible, et empêcheraient d'utiliser les techniques de filtrage de texture que nous verrons dans ce chapitre.
À partir de ces coordonnées de texture, la carte graphique calcule l'adresse du texel qui correspond, et se charge de le lire. Et toute la magie a lieu dans ce calcul d'adresse, qui part de coordonnées de texture flottante, pour arriver à une adresse mémoire. Le calcul de l'adresse du texel se fait en plusieurs étapes, que nous allons voir ci-dessous. La première étape convertit les coordonnées flottantes en coordonnées entières, qui disent à quel ligne et colonne se trouve le texel voulu dans la texture. L'étape suivante transforme ces coordonnées x,y entières en adresse mémoire.
===La normalisation des coordonnées===
J'ai dit plus haut que les coordonnées de texture sont des coordonnées flottantes, comprises entre 0 et 1. Mais il faut savoir que les pixels shaders peuvent modifier celles-ci pour mettre en œuvre certains effets graphiques. Et le résultat peut alors se retrouver en-dehors de l'intervalle 0,1. C'est quelque chose de voulu et qui est traité par la carte graphique automatiquement, sans que ce soit une erreur. Au contraire, la manière dont la carte graphique traite cette situation permet d'implémenter des effets graphiques comme des textures en damier ou en miroir.
[[File:Clamp tile.jpg|vignette|Clamp tile]]
Il existe globalement trois méthodes très simples pour gérer cette situation, qui sont appelés des '''modes d'adressage de texture'''.
* La première méthode est de faire en sorte que le résultat sature. Si une coordonnée est inférieur à 0, alors on la remplace par un zéro. Si elle est supérieure à 1, on la ramène à 1. Avec cette méthode, tout se passe comme si les bords de la texture étaient étendus et remplissaient tout l'espace autour de la texture. Le tout est illustré ci-dessous. Ce mode d'accès aux textures est appelé le '''''clamp'''''.
* Une autre solution retire la partie entière de la coordonnée, elle coupe tout ce qui dépasse 1. Pour le dire autrement, elle calcule le résultat modulo 1 de la coordonnée. Le résultat est que tout se passe comme si la texture était répétée à l'infini et qu'elle pavait le plan.
* Une autre méthode remplit les coordonnées qui sortent de l’intervalle 0,1 avec une couleur préétablie, configurée par le programmeur.
===La conversion des coordonnées de textures flottantes en adresse mémoire===
Une fois la normalisation effectuée, les coordonnées de texture sont utilisées pour lire le texel voulu. Pour cela, les coordonnées de texte sont transformées en adresse mémoire, adresse qui pointe sur le texel ayant ces cordonnées. Pour cela, la première étape est de transformer les coordonnées flottantes u,v en coordonnées entières x,y qui pointent sur un texel. Pour cela, il suffit de multiplier les coordonnées flottantes u,v par la résolution de la texture accédée. Pour un écran de résolution <math>\text{height,width}</math>, le calcul est le suivant :
: <math>x = u \times \text{width}</math>
: <math>y = v \times \text{height}</math>
Le résultat est un nombre avec une partie entière et une partie fractionnaire. La partie entière des deux coordonnées donne la position x,y voulue, et la partie fractionnaire est conservée pour le filtrage de textures, mais passons cela sous silence pour le moment.
La seconde étape prend les coordonnées entières x,y et calcule l'adresse mémoire du texel. L'adresse dépend de la position de la texture en mémoire, précisément de son début, son premier texel, mais aussi de la position du texel par rapport au début de la texture. Et calculer cette position intra-texture dépend de la manière dont les texels sont stockés en mémoire.
====Les textures naïves====
Les programmeurs qui lisent ce cours s'attendent certainement à ce que la texture soit stockée en mémoire ligne par ligne, ou colonne par colonne. Cela veut dire que le premier pixel en partant d'en haut à gauche est stocké en premier, puis celui immédiatement à sa droite, puis celui encore à droite, et ainsi de suite. Une fois qu'on arrive à la fin d'une ligne, on passe à la ligne suivante, en-dessous. Cette organisation ligne par ligne s'appele l'organisation '''''row major order'''''. On peut faire pareil, mais colonne par colonne, ce qui donne le '''''column major order'''''.
[[File:Speicheranordnung Feld.svg|centre|vignette|upright=2|Row et column major order.]]
Maintenant, supposons que la texture commence à l'adresse <math>A_\text{texture}</math>, qui est l'adresse du premier texel. La texture a une résolution de <math>\text{width}</math> texels de large et <math>\text{height}</math> texels de haut. Par définition, les coordonnées X et Y des texels commencent à 0, ce qui fait que le pixel en haut à gauche a les coordonnées 0,0.
L'adresse du pixel se calcule comme suit :
: <math>A_\text{pixel} = A_\text{texture} + (\text{taille d'une ligne en octets} \times Y) + (\text{taille d'un texel en octets} \times X)</math>
La taille d'un pixel en mémoire est notée T. La taille d'une ligne en mémoire est de <math>width \times T</math>, par définition, vu qu'elle fait <math>width</math> texels. On a donc :
: <math>A_\text{pixel} = A_\text{texture} + (width \times T \times Y) + (T \times X)</math>
La formule se réécrit comme suit :
: <math>A_\text{pixel} = A_\text{texture} + T \times (width \times Y + X)</math>
Le calcul d'adresse est donc assez simple. Malheureusement, les textures ne sont pas stockées de cette manière en mémoire vidéo. En effet, elle se marie mal avec les opérations de filtrage de texture que nous allons voir dans ce qui suit. Le filtrage d'un texel dépend de ses voisins du dessus et du dessous. Le fait que la texture n'est pas forcément parcourue ligne par ligne fait que stocker une texture ligne par ligne n'est pas l'idéal.
De même, les textures sont déformées par la perspective. L'affichage de la texture ne se fait alors pas ligne par ligne, mais en parcourant la texture en diagonale, l'angle de la diagonale correspondant approximativement à l'angle que fait la verticale de la texture avec le regard. Vu qu'on ne connait pas à l'avance l'angle que fera la diagonale de parcours, on doit ruser.
====Les textures tilées====
Une première solution à ce problème est celle des '''textures tilées'''. Avec ces textures, l'image de la texture est découpée en ''tiles'', des rectangles ou en carrés de taille fixe, généralement des carrés de 4 pixels de côté. Les tiles ont une largeur et une longueur égales, afin de simplifier les calculs : on divise X et Y par le même nombre. De plus, leur largeur et leur longueur sont une puissance de deux, afin de simplifier les calculs d'adresse. Les ''tiles'' sont alors mémorisée les unes après les autres dans le fichier de la texture.
[[File:Texture tilée.png|centre|vignette|upright=2|Texture tilée]]
La formule de calcul d'adresse vue plus haut doit être adaptée pour tenir compte des tiles. Pour cela, il faut remplacer la taille d'un texel par la taille d'une tile, et que la largeur de la texture soit exprimée en nombre de tiles. De plus, on doit adapter les coordonnées des texels pour donner des coordonnées de tile. Généralement, les tiles sont des carrés de N pixels de côté, ce qui fait qu'on peut regrouper les lignes et les colonnes par paquets de N. Il suffit donc de diviser Y et X pour obtenir les coordonnées de la tile, de même que la larguer. La formule pour calculer la position de la énième tile est alors la suivante :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + \text{Taille mémoire d'une tile} \times \left( {\text{Width} \over N} \times {Y \over N} + {X \over N} \right)</math>
On peut réécrire le tout comme suit :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + K \times \left( {Y \over N} + X \right)</math>, avec K une constante connue à la compilation des shaders.
Vu que les tiles sont carrées avec une largeur qui est une puissance de deux, la multiplication par la taille d'une tile en mémoire se simplifie : on passe d'une multiplication entière à des décalages de bits. Même chose pour le calcul de l'adresse de la tile à partir des coordonnées x,y : ils impliquent des divisions par une puissance de deux, qui deviennent de simples décalages.
La position d'un pixel dans une tile dépend du format de la texture, mais peut se calculer avec quelques calculs arithmétiques simples. Dans les cas les plus simples, les pixels sont mémorisés ligne par ligne, ou colonne par colonne. Mais ce n'est pas systématiquement le cas. Toujours est-il que les calculs pour déterminer l'adresse sont simples, et ne demandent que quelques additions ou multiplications. Mais avec les formats de texture utilisés actuellement, les tiles sont chargées en entier dans le cache de texture, sans compter que diverses techniques de compression viennent mettre le bazar, comme on le verra dans la suite de cours.
Un avantage de l'organisation en tiles est qu'elle se marie bien avec le parcours des textures. On peut parcourir une texture dans tous les sens, horizontal, vertical, ou diagonal, on sait que les prochains pixels ont de fortes chances d'être dans la même tile. Si on rentre dans une tile par la gauche en haut, on a encore quelques pixels à parcourir dans la tile, par exemple. De même, le filtrage de textures est facilité. On verra dans ce qui va suivre que le filtrage de texture a besoin de lire des blocs de 4 texels, des carrés de 2 pixels de côté. Avec l'organisation en tile, on est certain que les 4 texels seront dans la même tile, sauf s'ils ont le malheur d'être tout au bord d'une tile. Ce dernier cas est assez rare, et il l'est d'autant plus que les tiles sont grandes. Enfin, un dernier avantage est que les tiles sont généralement assez petites pour tenir tout entier dans une ligne de cache. Le cache de texture est donc utilisé à merveille, ce qui rend les accès aux textures plus rapides.
====Les textures basées sur des ''z-order curves''====
Les formats de textures théoriquement optimaux utilisent une '''''Z-order curve''''', illustrée ci-dessous. L'idée est de découper la texture en quatre rectangles identiques, et de stocker ceux-ci les uns à la suite des autres. L'intérieur de ces rectangles est lui aussi découpé en quatre rectangles, et ainsi de suite. Au final, l'ordre des pixels en mémoire est celui illustré ci-dessous.
[[File:Z-CURVE.svg|centre|vignette|upright=2|Construction d'une ''Z-order curve''.]]
Les texels sont stockés les uns à la suite des autres dans la mémoire, en suivant l'ordre donnée par la ''Z-order curve''. Le calcul d'adresse calcule la position du texel en mémoire, par rapport au début de la texture, et ajoute l'adresse du début de la texture. Mais tout le défi est de calculer la position d'un texel en mémoire, à partir des coordonnées x,y. Le calcul peut sembler très compliqué, mais il n'en est rien. Le calcul demande juste de regarder les bits des deux coordonnées et de les combiner d'une manière particulièrement simple. Il suffit de placer le bit de poids fort de la coordonnée x, suivi de celui de la coordonnée y, et de faire ainsi de suite en passant aux bits suivants.
[[File:Zcurve45bits.png|centre|vignette|upright=1.5|Calcul de la position d'un élément dans une ''Z-order curve'' à partir des coordonnées x et y.]]
L'avantage d'une telle organisation est que la textures est découpées en ''tiles'' rectangulaires d'une certaine taille, elles-mêmes découpées en ''tiles'' plus petites, etc. Et il se trouve que cette organisation est parfaite pour le cache de texture. L'idéal pour le cache de texture est de charger une ''tile'' complète dans le cache de textures. Quand on accède à un texel, on s'assure que la ''tile'' complète soit chargée. Mais cela demande de connaitre à l'avance la taille d'une ''tile''. Les formats de texture fournissent généralement une ''tile'' carré de 4 pixels de côté, mais cela donnerait un cache trop petit pour être vraiment utile. Avec cette méthode, on s'assure qu'il y ait une ''tile'' avec la taille optimale. Les ''tiles'' étant découpées en ''tiles'' plus petites, elles-mêmes découpées, et ainsi de suite, on s'assure que la texture est découpées en ''tiles'' de taille variées. Il y aura au moins une ''tile'' qui rentrera tout pile dans le cache.
==Les techniques de rendu à textures multiples==
Nous venons de voir comment une texture est plaquée sur un objet 3D, ou une surface comme un sol. Pour résumer, le calcul de l'adresse d'un texel prend la position du texel par rapport au début de la texture, et ajoute l'adresse du début de la texture. L'adresse mémoire de la texture est connue au moment où le pilote de la carte graphique place la texture dans la mémoire vidéo, et cette information est transmise au matériel par l'intermédiaire du processeur de commande, puis passée aux processeurs de shaders et à l'unité de texture. Le tout est couplé à d'autres informations, la plus importante étant la ''taille de la texture en octets'', pour éviter de déborder lors des accès à la texture.
Néanmoins, il s'agit là du cas le plus simple. Certaines techniques de rendu demandent de choisir la texture à plaquer parmi un ensemble de plusieurs textures. Les techniques en question sont assez variées et n'ont pas grand chose en commun. Les plus connues sont le ''mip-mapping'', le ''cube-mapping'' et les textures virtuelles. Le ''mip-mapping'' sert à filtrer les textures, chose qu'on expliquera plus tard, le ''cube-mapping'' sert à simuler des réflexions sur un objet en plaquant une texture de l'environnement dessus, les textures virtuelles sont une optimisation pour les textures des terrains de grande taille. Mais malgré leurs différences, elles demandent de choisir quelle texture plaquer entre plusieurs textures de base. En clair, l'adresse de base de la texture varie selon la situation. Voyons-les dans le détail.
===Le mip-mapping===
Le '''mip-mapping''' a pour but de légèrement améliorer les graphismes des objets lointains, tout en rendant les calculs de texture plus rapides. Formellement, le ''mip-mapping'' est une technique de filtrage de texture, mais nous l'abordons maintenant car elle est surtout liée au calcul d'adresse. Les unités de texture ont des circuits de filtrage de texture séparés des circuits de ''mip-mapping'' et de calcul d'adresse, d'où le fait que nous en parlons séparément.
Le problème résolu par le ''mip-mapping'' est le rendu des textures lointaines. Si une texture est plaquée sur un objet lointain, une bonne partie des détails est invisible pour l'utilisateur. Un pixel de l'écran est associé à plusieurs texels. Idéalement, la carte graphiques devrait lire tous ces texels et en faire une sorte de moyenne pondérée, pour calculer la couleur finale du pixel. Mais dans les faits, ce serait très gourmand et compliqué à implémenter en hardware. Une solution serait de ne garder que quelque texels, mais cela a tendance à créer des artefacts visuels (les textures affichées ont tendance à pixeliser). Le ''mip-mapping'' permet de réduire ces deux problèmes en même temps en précalculant cette moyenne pondérée pour des distances prédéfinies.
L'idée est d'utiliser plusieurs exemplaires d'une même texture à des résolutions différentes, chaque exemplaire étant adapté à une certaine distance. Par exemple, une texture sera stocké avec un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre de 128 * 128 et ainsi de suite jusqu’à un dernier exemplaire de 32 * 32 pixel. Chaque exemplaire correspond à un '''niveau de détail''', aussi appelé ''Level Of Detail'' (abrévié en LOD). La résolution utilisée diminue d'autant plus que l'objet est situé loin de la caméra. Les objets proches seront rendus avec la texture 512*512, ceux plus lointains seront rendus avec la texture de résolution 256*256, les textures 128*128 seront utilisées encore plus loin, et ainsi de suite jusqu'aux objets les plus lointains qui sont rendus avec la texture la plus petite de 32*32.
[[File:MipMap Example STS101.jpg|centre|vignette|upright=2|Exemples de mip-maps.]]
Le ''mip-mapping'' améliore grandement la qualité d'image. L'image d'exemple ci-dessous le montre assez bien.
[[File:Mipmapping example.png|centre|vignette|upright=2|Exemple de mipmapping.]]
Pour faciliter les calculs d'adresse, les LOD d'une même texture sont stockées les uns après les autres en mémoire (dans un tableau, comme diraient les programmeurs). Ainsi, pas besoin de se souvenir de la position en mémoire de chaque LOD : l'adresse de la texture de base, et quelques astuces arithmétiques suffisent. Prenons le cas où la texture de base a une taille L. le premier exemplaire est à l'adresse 0, le second niveau de détail est à l'adresse L, le troisième à l'adresse L + L/4, le suivant à l'adresse L + L/4 + L/16, et ainsi de suite. Le calcul d'adresse demande juste connaître le niveau de détails souhaité et l'adresse de base de la texture. Le niveau de détail voulu est calculé par les pixel shaders, en fonction de la coordonnée de profondeur du pixel à traiter.
Évidemment, cette technique consomme de la mémoire vidéo, vu que chaque texture est dupliquée en plusieurs exemplaires, en plusieurs LOD. Dans le détail, la technique du mip-mapping prend au maximum 33% de mémoire en plus (sans compression). Cela vient du fait qu'en prenant une texture dexu fois plus petite, elle prend 4 fois moins de mémoire : 2 fois moins de pixels en largeur, et 2 fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des LODs, texture de base comprise, prendra X + (X/4) + (X/16) + (X/256) + … Un petit calcul de limite donne 4/3 * X, soit 33% de plus.
===Le cube-mapping===
[[File:Cube mapped reflection example 2.JPG|vignette|Exemple de reflets environnementaux.]]
L''''environnement-mapping''' est une technique de calcul de divers effets graphiques liés à l'environnement, notamment des réflexions. L'idée est de plaquer une texture pré-calculée pour simuler l'effet de l'environnement sur une surface ou un objet 3D. Il en existe plusieurs versions différentes, mais la seule utilisée de nos jours est le ''cube-mapping'', où la texture de l'environnement est plaquée sur un cube, d'où son nom. Le cube en question est utilisé différemment suivant ce que l'on cherche à faire avec le ''cube-mapping''. Les deux utilisations principales sont le rendu du ciel et des décors, et les réflexions sur la surface des objets. Dans les deux cas, l'idée est de précalculer ce que l'on voit du point de vue de la caméra. On place la caméra dans la scène 3D, on place un cube centré sur la caméra, le cube est texturé avec ce que l'on voit de l'environnement depuis la caméra/l'objet de son point de vue.
[[File:Panorama cube map.png|centre|vignette|upright=2|L'illustration montre en premier lieu une ''cubemap'' avec les six faces mises en évidence, puis quel environnement 3D elle permet de simuler, le troisième illustration montrant comment la ''cubemap'' est utilisée pour simuler l'environnement.]]
Le rendu du ciel et des décors lointains dans les jeux vidéo se base sur des '''''skybox''''', à savoir un cube centré sur la caméra, sur lequel on ajoute des textures de ciel ou de décors lointains. Le cube est recouvert par une texture, qui correspond à ce que l'on voit quand on dirige le regard de la caméra vers cette face. Contrairement à ce qu'on pourrait croire, la skybox n'est pas les limites de la scène 3D, les limites du niveau d'un jeu vidéo ou quoique ce soit d'autre de lié à la physique de la scène 3D. La skybox est centrée sur la caméra, elle suit la caméra dans son mouvement. Centrer la skybox sur la caméra permet de simuler des décors très lointains, suffisamment lointain pour qu'on n'ait pas l'illusion de s'en rapprocher en se déplaçant dans la map. De plus, cela évite d'avoir à faire trop de calculs à chaque fois que l'on bouge la caméra. La texture plaquée sur le cube est une texture unique, elle-même découpée en six sous-textures, une par face du cube.
[[File:Skybox example.png|centre|vignette|upright=2|Exemple de Skybox.]]
[[File:Cube mapped reflection example.jpg|vignette|Réflexions calculées par une ''cubemap''.]]
Le ''cube-mapping'' est aussi utilisé pour des reflets. L'idée est de simuler les reflets en plaquant une texture pré-calculée sur l'objet réflecteur. La texture pré-calculée est un dessin de l'environnement qui se reflète sur l'objet, un dessin du reflet à afficher. En la plaquant la texture sur l'objet, on simule ainsi des reflets de l'environnement, mais on ne peut pas calculer d'autres reflets comme les reflets objets mobiles comme les personnages. Et il se trouve que la texture pré-calculée est une ''cubemap''. Pour les environnements ouverts, c'est la ''skybox'' qui est utilisée, ce qui permet de simuler les reflets dans les flaques d'eau ou dans des lacs/océans/autres. Pour les environnements intérieurs, c'est une cubemap spécifique qui utilisée. Par exemple, pour l'intérieur d'une maison, on a une ''cubemap'' par pièce de la maison. Les reflets se calculent en précisant quelle ''cubemap'' appliquer sur l'objet en fonction de la direction du regard.
[[File:Cube map level.png|centre|vignette|Cube map de l'intérieur d'une pièce d'un niveau de jeux vidéo.]]
Toujours est-il que les textures utilisées pour le ''cubemmapping'', appelées des ''cubemaps'', sont en réalité la concaténation de six textures différentes. En mémoire vidéo, la ''cubemap'' est stockée comme six textures les unes à la suite des autres. Lors du rendu, on doit préciser quelle face du cube utiliser, ce qui fait 6 possibilités. On a le même problème qu'avec les niveaux de détail, sauf que ce sont les faces d'une ''cubemap'' qui remplacent les textures de niveaux de détails. L'accès en mémoire doit donc préciser quelle portion de la ''cubemap'' il faut accéder. Et l'accès mémoire se complexifie donc. Surtout que l'accès en question varie beaucoup suivant l'API graphique utilisée, et donc suivant la carte graphique.
Les API 3D assez anciennes ne gérent pas nativement les ''cubemaps'', qui doivent être émulées en logiciel en utilisant six textures différentes. Le pixel shader décide donc quelle ''cubemap'' utiliser, avec quelques calculs sur la direction du regard. L'accès se fait d'une manière assez simple : le shader choisit quelle texture utiliser. Les API 3D récentes gèrent nativement les ''cubemaps''. Dans le cas le plus simple,pour les versions les plus vielles de ces API, les six faces sont numérotées et l'accès à une ''cubemap'' précise quel face utiliser en donnant son numéro. La carte graphique choisit alors automatiquement la bonne texture, mais cela demande de laisser le calcul de la bonne face au pixel shader. D'autres API 3D et cartes graphiques font autrement. Dans les API 3D modenres, les ''cubemap'' sont gérées comme des textures en trois dimensions, adressées avec trois coordonnées u,v,w. La carte graphique utilise ces trois coordonnées de manière à en déduire quelle est la face pertinente, mais aussi les coordonnées u,v dans la texture de la face.
==L'implémentation matérielle du placage de textures==
Pour résumer, la lecture d'un texel demande d'effectuer plusieurs étapes. Dans le cas le plus simple, sans ''mip-mapping'' ou ''cubemapping'', on doit effectuer les étapes suivantes :
* Il faut d'abord normaliser les coordonnées de texture pour qu'elles tombent dans l'intervalle [0,1] en fonction du mode d'adressage désiré.
* Ensuite, les coordonnées u,v doivent être converties en coordonnées entières, ce qui demande une multiplication flottante.
* Enfin, l'adresse finale est calculée à partir des coordonnées entières et en ajoutant l'adresse de base de la texture (et éventuellement avec d'autres calculs arithmétiques suivant le format de la texture).
Tout cela pourrait être fait par le pixel shaders, mais cela implique beaucoup de calculs répétitifs et d'opérations arithmétiques assez lourdes, avec des multiplications flottantes, des additions et des multiplications entières, etc. Faire faire tous ces calculs par les shaders serait couteux en performance, sans compter que les shaders deviendraient plus gros et que cela aurait des conséquences sur le cache d'instruction. De plus, certaines de ces étapes peuvent se faire en parallèle, comme les deux premières, ce qui colle mal avec l'aspect sériel des shaders.
Aussi, les processeurs de shaders incorporent une unité de calcul d'adresse spéciale pour faire ces calculs directement en matériel. L'unité de texture contient au minimum deux circuits : un circuit de calcul d'adresse, et un circuit d'accès à la mémoire. Toute la difficulté tient dans le calcul d'adresse, plus que dans le circuit de lecture. Le calcul d'adresse est conceptuellement réalisé en deux étapes. La première étape qui transforme les coordonnées u,v en coordonnées x,y qui donne le numéro de la ligne et de la colonne du texel dans la texture. La seconde étape prend ces deux coordonnées x,y, l'adresse de la texture, et détermine l'adresse de la tile à lire.
[[File:Unité de texture simple.png|centre|vignette|upright=2|Unité de texture simple]]
===L'implémentation du mip-mapping===
Le ''mip-mapping'' est lui aussi pris en charge par l'unité de calcul d'adresse, car cette technique change l'adresse de base de la texture. La gestion du ''mip-mapping'' est cependant assez complexe. Il est possible de laisser le pixel shader calculer quel niveau de détail utiliser, en fonction de la coordonnée de profondeur z du pixel à afficher. La carte graphique détermine alors automatiquement quelle texture lire, quel niveau de détail, automatiquement. Elle détermine aussi la bonne résolution pour la texture, qui est égal à la résolution de la texture de base, divisée par le niveau de détail. Pour résumer, le niveau de détail est envoyé aux unités de texture, qui s'occupent de calculer l'adresse de base et la résolution adéquates. Quelques calculs arithmétiques simples, donc, qui s'implémentent facilement avec quelques circuits.
Mais une autre méthode laisse la carte graphique déterminer le niveau de détail par elle-même. Dans ce cas, cela demande, outre les deux coordonnées de texture, de calculer la dérivée de ces deux coordonnées dans le sens horizontal et vertical, ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Les quatre dérivées sont les suivantes :
: <math>\frac{du}{dx}</math>, <math>\frac{dv}{dx}</math>, <math>\frac{du}{dy}</math>, <math>\frac{dv}{dy}</math>
Un bon moyen pour obtenir les dérivées demande de regrouper les pixels par groupes de 4 et de faire la différence entre leurs coordonnées de texture respectives. On peut calculer les deux dérivées horizontales en comparant les deux pixels sur la même ligne, et les deux dérivées verticales en comparant les deux pixels sur la même colonne. Mais cela demande de rastériser les pixels par groupes de 4, par ''quads''. Et c'est ce qui est fait sur les cartes graphiques actuelles, qui rastérisent des groupes de 4 pixels à la fois.
[[File:Texture sampler unit with mipmapping.png|centre|vignette|upright=2.0|Unité de texture avec mipmapping.]]
Malheureusement, le calcul exact utilisé pour le choix de la mip-map dépend du GPU considéré et peu de chose est connu quant à ces algorithmes. Il est possible d'inférer le comportement à partir d'observations, mais guère plus. Pour ceux qui veulent en savoir plus, je conseille la lecture de cet article de blog :
* [https://pema.dev/2025/05/09/mipmaps-too-much-detail/ Mipmap selection in too much detail]
===La gestion des accès mémoire===
Enfin, l'unité de texture doit tenir compte du fait que la mémoire vidéo met du temps à lire une texture. En théorie, l'unité de texture ne devrait pas accepter de nouvelle demande de lecture tant que celle en cours n'est pas terminée. Mais faire ainsi demanderait de bloquer tout le pipeline, de l'''input assembler'' au unités de''shaders'', ce qui est tout sauf pratique et nuirait grandement aux performances.
Une solution alternative consiste à mettre en attente les demandes de lectures de texture pendant que la mémoire est occupée. La manière la plus simple d'implémenter des accès mémoire multiples est de les mettre en attente dans une petite mémoire FIFO. Cela implique que les accès mémoire s’exécutent dans l'ordre demandé par le ''shader'' et/ou l'unité de rastérisation, il n'y a pas de réorganisation des accès mémoire ou d’exécution dans le désordre des accès mémoire.
[[File:Texture prefetching.png|centre|vignette|upright=1.5|Accès mémoire simultanés.]]
Évidemment, quand la mémoire FIFO est pleine, le pipeline est alors totalement bloqué. Le rasteriser est prévenu que l'unité de texture ne peut pas accepter de nouvelle lecture de texture. En pratique, la FIFO est généralement d'une taille respectable et permet de mettre en attente beaucoup de demandes de lecture de texture. Il faut de plus noter qu'il y a une FIFO par processeur de ''shader'' sur les cartes graphiques modernes. Quand elle est pleine, le processeur cesse d'exécuter de nouveaux accès mémoire, mais peut continuer à exécuter des ''shaders'' dans les autres unités de calcul, pas besoin de bloquer complétement le pipeline.
===L'intégration du cache de textures===
Il faut noter que les unités de texture incorporent aussi un cache de texture, voire plusieurs. L'intégration des caches de texture avec la mémoire FIFO précédente est quelque peu compliqué, car il faut garantir que les lectures de texture se fassent dans le bon ordre. On ne peut pas exécuter une lecture dans le cache alors que des lectures précédentes sont en attente de lecture en mémoire vidéo. Et cela pose un gros problème : une lecture dans le cache de texture prend quelques dizaines de cycles d'horloge, alors qu'une lecture en mémoire vidéo en prend facilement 400 à 800 cycles, parfois plus. Et cela fait que l'ordre des accès mémoire peut s'inverser.
Prenons par exemple un accès au cache précédé et suivi par deux accès en mémoire vidéo. Le premier démarre au cycle 1, et se termine au cycle numéro 400. L'accès au cache commence au cycle 2 et se termine 20 cycles après, au cycle numéro 22. En clair, la lecture dans le cache s'est terminée avant l'accès mémoire qui le précède. Les textures ne sont donc plus lues dans l'ordre. Et il faut trouver une solution pour éviter cela.
La solution est de retarder les lectures dans le cache tant que tous les accès précédents ne sont pas terminés. Mais pour retarder les lectures en question, il faut d'abord savoir si la lecture atterrit dans le cache ou non, ce qui demande d'accéder au cache. On fait face à un dilemme : on veut retarder les accès au cache, mais les différencier des lectures déclenchant des accès mémoire demande d'accéder au cache en premier lieu. La solution est décrite dans l'article "Prefetching in a Texture Cache Architecture" par Igehy et ses collègues. Elle se base sur deux idées combinées ensemble.
La première idée est de séparer l'accès au cache en deux : une étape qui vérifie si les texels à lire sont dans le cache, et une étape qui accède aux données dans le cache lui-même. Un cache de texture est donc composé de deux circuits principaux. Le premier vérifie la présence des texels dans le cache. Il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Ensuite, en plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
La seconde idée est de retarder l'accès au cache entre les deux phases. La première étape d'un accès mémoire vérifie si la donnée est dans le cache ou non. Puis, on retarde la lecture des données, pour attendre que toutes les lectures précédentes soient terminées. Et enfin, troisième étape : la lecture des texels dans la mémoire cache proprement dite. Les accès mémoire passant par la mémoire vidéo se font de la même manière, à une différence près : la lecture dans le cache est remplacée par la lecture en mémoire vidéo. Tout démarre avec une demande à l'unité de tags, qui vérifie si le texel est dans le cache ou non. Puis on retarde l'accès tant que la mémoire vidéo est occupée, puis on effectue la lecture en mémoire vidéo.
Si ce n'est pas le cas, l'accès mémoire est envoyé à la mémoire vidéo comme précédemment, à savoir qu'il est mis en attente dans une mémoire FIFO, puis envoyé à la mémoire vidéo dès que celle-ci est libre. Mais en sortie de la mémoire, la donnée lue est envoyée dans le cache de texture, par dans l'unité de filtrage. Pour savoir où placer la donnée lue, l'unité de tag a réservé une ligne de cache précise, une adresse bien précise. L'adresse en question est disponible en lisant une autre mémoire FIFO, qui a mis en attente l'adresse en question, en attendant que l'accès mémoire se termine. La donnée est alors écrite dans le cache, puis lue par l'unité de filtrage de textures.
Pour une lecture dans le cache, le déroulement est similaire, mais sans le passage par la mémoire. La lecture fait une demande à l'unité de tag, et celle-ci répond que la donnée est bien dans le cache. Elle place alors l'adresse à lire dans la file d'attente. Une fois que les accès mémoire précédents sont terminés, l'adresse sort de la file d'attente et est envoyée à la mémoire de données. La lecture s'effectue, les texels sont envoyés à l'unité de filtrage de textures. La seule différence avec un ''phased cache'' normal est l'insertion de l'adresse à lire dans une FIFO qui vise à mettre en attente
[[File:Unité de texture avec un cache de texture.png|centre|vignette|upright=2.0|Unité de texture avec un cache de texture]]
Pour résumer, l'implémentation précédente garantit une exécution des lectures dans leur ordre d'arrivée. Et pour cela, elle retarde les lectures dans le cache tant que les lectures en mémoire précédentes ne sont pas terminées. L'accès au cache est plus rapide que l'accès en mémoire vidéo, mais le retard ajouté pour garantir l'ordre des lectures fait que le temps d'accès est très long.
==Le filtrage de textures==
Plaquer des textures sans autre forme de procès ne suffit pas à garantir des graphismes d'une qualité époustouflante. La raison est que les sommets et les texels ne tombent pas tout pile sur un pixel de l'écran : le sommet associé au texel peut être un petit peu trop en haut, ou trop à gauche, etc. Une explication plus concrète fait intervenir les coordonnées de texture. Souvenez-vous que lorsque l'on traduit une coordonnée de texture u,v en coordonnées x,y, on obtient un résultat qui ne tombe pas forcément juste. Souvent, le résultat a une partie fractionnaire. Si celle-ci est non-nulle, cela signifie que le texel/sommet n'est pas situé exactement sur le pixel voulu et que celui-ci est situé à une certaine distance. Concrètement, le pixel tombe entre quatre texels, comme indiqué ci-dessous.
[[File:Filtrage texture.png|centre|vignette|upright=2.0|Position du pixel par rapport aux texels.]]
Pour résoudre ce problème, on doit utiliser différentes techniques d'interpolation, aussi appelées techniques de '''filtrage de texture''', qui visent à calculer la couleur du pixel final en fonction des texels qui l'entourent. Il existe de nombreux types de filtrage de textures, qu'il s'agisse du filtrage linéaire, bilinéaire, trilinéaire, anisotropique et bien d'autres.
Tous ont besoin d'avoir certaines informations qui sont généralement fournies par les circuits de calcul d'adresse. La première est clairement la partie fractionnaire des coordonnées x,y. La seconde est la dérivée de ces deux coordonnées dans le sens horizontal et vertical., ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Toujours est-il que le filtrage de texture est une opération assez lourde, qui demande beaucoup de calculs arithmétiques. On pourrait en théorie le faire dans les pixels shaders, mais le cout en performance serait absolument insoutenable. Aussi, les cartes graphiques intègrent toutes un circuit dédié au filtrage de texture, le ''texture sampler''. Même les plus anciennes cartes graphiques incorporent une unité de filtrage de texture, ce qui nous montre à quel point cette opération est importante.
[[File:Texture unit.png|centre|vignette|upright=2.0|Unité de texture.]]
On peut configurer la carte graphique de manière à ce qu'elle fasse soit du filtrage bilinéaire, soit du filtrage trilinéaire, on peut configurer le niveau de filtrage anisotropique, etc. Cela peut se faire dans les options de la carte graphique, mais cela peut aussi être géré par l'application. La majorité des jeux vidéos permettent de régler cela dans les options. Ces réglages ne concernent pas la texture elle-même, mais plutôt la manière dont l'unité de texture doit fonctionner. Ces réglages sur l''''état de l'unité de texture''' sont mémorisés quelque part, soit dans l'unité de texture elle-même, soit fournies avec la ressource de texture elle-même, tout dépend de la carte graphique. Certaines cartes graphiques mémorisent ces réglages dans les unités de texture ou dans le processeur de commande, et tout changement demande alors de réinitialiser l'état des unités de texture, ce qui prend un peu de temps. D'autres placent ces réglages dans les ressources de texture elles-mêmes, ce qui rend les modifications de configuration plus rapides, mais demande plus de circuits. D'autres cartes graphiques mélangent les deux options, certains réglages étant globaux, d'autres transmis avec la texture. Bref, difficile de faire des généralités, tout dépend du matériel et le pilote de la carte graphique cache tout cela sous le tapis.
Maintenant que cela est dit, voyons quelles sont les différentes méthodes de filtrage de texture et comment la carte graphique fait pour les calculer.
===Le filtrage au plus proche===
La méthode de filtrage la plus simple consiste à colorier avec le texel le plus proche. Cela revient tout simplement à ne pas tenir compte de la partie fractionnaire des coordonnées x,y, ce qui est très simple à implémenter en matériel. C'est ce que l'on appelle le '''filtrage au plus proche''', aussi appelé ''nearest filtering''.
Autant être franc, le résultat est assez pixelisé et peu agréable à l’œil. Par contre, le résultat est très rapide à calculer, vu qu'il ne demande aucun calcul à proprement parler. Elle ne fait pas appel à la parti fractionnaire des coordonnées entières de texture, ni aux dérivées de ces coordonnées. On peut combiner cette technique avec le mip-mapping, ce qui donne un résultat bien meilleur, bien que loin d'être satisfaisant. Au passage, toutes les techniques de filtrage de texture peuvent se combiner avec du mip-mapping, certaines ne pouvant pas faire sans.
[[File:Interpolation-nearest.svg|centre|vignette|Filtrage de texture au plus proche.]]
===Le filtrage linéaire===
Le filtrage le plus simple est le '''filtrage linéaire'''. Il effectue une interpolation linéaire entre deux mip-maps, deux niveaux de détails. Pour comprendre l'idée, nous allons prendre une situation très simple, avec une texture carrée de 512 texels de côté. Le mip-mapping crée plusieurs textures : une de 256 texels de côté, une de 128 texels, une de 64, etc. Maintenant, la texture est sur un objet à une certaine distance de l'écran, vu de face. Le résultat est qu'elle correspond à l'écran à un carré de 300 pixels de côté (pas d'erreur : pixels, pas texels). Dans ce cas, la texture se trouve entre deux mip-maps : celle de 512 pixels de côté, celle de 256. Laquelle choisir ? Le filtrage au plus proche prend la texture de 512 pixels de côté. Le filtrage linéaire lui, fait autrement.
Vu que la texture est entre deux mip-maps, l'idée est de prendre le texel au plus proche dans chaque texture et de faire une sorte de moyenne appelée l'interpolation linéaire. L'interpolation par du principe que la couleur varie entre les deux texels en suivant une fonction affine, illustrée ci-dessous. Ce ne serait évidemment pas le cas dans le monde réel, mais on supposer cela donne une bonne approximation de ce à quoi ressemblerait une texture à plus haute résolution. On peut alors calculer la couleur du pixel par une simple moyenne pondérée par la distance. Le résultat est que les transitions entre deux niveaux de détails sont plus lisses, moins abruptes.
[[File:Lin interp -é.png|centre|vignette|upright=2.0|Interpolation linéaire.]]
===Le filtrage bilinéaire===
Le filtrage bilinéaire effectue une sorte de moyenne pondérée des quatre texels les plus proches du pixel à afficher. Pour cela, rappelez-vous ce qui a été dit plus haut : les coordonnées x,y d'un pixel ont une partie entière et une partie fractionnaire. Le filtrage au plus proche élimine les parties fractionnaires, ce qui donne une coordonnée x,y. Avec le filtrage bilinéaire, on prend les texels de coordonnées (x,y) ; (x+1,y) ; (x,y+1) ; (x+1,y+1), le pixel étant entre ces 4 texels.
Mais le filtrage ne fait pas qu'une simple moyenne, il prend en compte les parties fractionnaires pour faire la moyenne. En effet, le pixel n'est pas au milieu du carré de texel, il est quelque part mais est souvent plus proche d'un texel que des autres. Et il faut donc pondérer la moyenne par les distances aux 4 texels. Pour cela, la moyenne est calculée à partir d'interpolations linéaires. Avec 4 pixels, nous allons devoir calculer la couleur de deux points intermédiaires. La couleur de ces deux points se calcule par interpolation linéaire, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.
[[File:Bilin3.png|centre|vignette|upright=2|Filtrage bilinéaire de texture.]]
Le circuit qui permet de faire l'interpolation bilinéaire est particulièrement simple. On trouve un circuit de chaque pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu, et un pour la transparence. Chacun de ces circuit est composé de sous-circuits chargés d'effectuer une interpolation linéaire, reliés comme suit.
[[File:Texture sampler unit.png|centre|vignette|Unité de filtrage bilinéaire.]]
Vous noterez que le filtrage bilinéaire accède à 4 pixels en même temps. Fort heureusement, les textures sont stockées de manière à ce qu'on puisse charger les 4 pixels en une fois, comme on l'a vu plus haut. Le filtrage bilinéaire a de fortes chances que les 4 pixels filtrés soient dans la même ''tile'', la seule exception étant quand ils sont tout juste sur le bord d'une ''tile''.
: La console de jeu Nintendo 64 n'utilise que trois pixels au lieu de quatre dans son interpolation bilinéaire, qui en devient une interpolation quasi-bilinéaire. La raison derrière ce choix est une question de performances, comme beaucoup de décisions de ce genre. Le résultat est un rendu imparfait de certaines textures.
===Le filtrage trilinéaire===
Avec le filtrage bilinéaire, des discontinuités apparaissent sur certaines surfaces. Par exemple, pensez à une texture de sol : elle est appliquée plusieurs fois sur toute la surface du sol. A une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512*512 à 256*256, ce qui est visible pour un joueur attentif. De telles transitions sont lissées grâce au filtrage linéaire, il n'y a plus qu'à le combiner avec le filtrage bilinéaire. Rien d’incompatible : le premier filtre l'intérieur d'une mip-map, le second combine deux mip-maps.
Le filtrage trilinéaire prend les deux mip-maps les plus proches, fait un filtrage bilinéaire avec chacune, puis fait une « une moyenne » pondérée entre les deux résultats. Le circuit de filtrage trilinéaire existe en plusieurs versions. La plus simple, illustrée ci-dessous, effectue deux filtrages bilinéaires en parallèle, dans deux circuits séparés, puis combine leurs résultats avec un circuit d'interpolation linéaire. Mais ce circuit nécessite de charger 8 texels simultanément. Qui plus est, ces 8 texels ne sont pas consécutifs en mémoire, car ils sont dans deux niveaux de détails/mip-maps différents.
[[File:Parallel trilinear filtering.png|centre|vignette|upright=2.0|Unité de filtrage trilinéaire parallèle.]]
Vu qu'on lit des texels dans deux mip-maps, les texels sont lus en deux fois : 4 texels provenant de la première mip-map, suivis par les 4 texels de l'autre mip-map. Les 4 premiers texels doivent donc être mis en attente dans des registres, en attendant que les 4 autres arrivent. Une amélioration du circuit précédent gère cela en ajoutant des registres. Il lit les 4 premiers texels, les filtre avec une interpolation bilinéaire, et mémorise le résultat dans un registre. Puis, il lit les 4 autres texels, les filtre, et met le résultat dans un second registre. A ce moment là, un circuit d'interpolation linéaire finit le travail. On économise donc un circuit d'interpolation bilinéaire, sans que les performances soient trop impactées.
[[File:Filtrage trilineaire.png|centre|vignette|upright=1.0|Unité de filtrage trilineaire série.]]
Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le temps où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. À chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients, et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.
===Le filtrage anisotrope===
D'autres artefacts peuvent survenir lors de l'application d'une texture, la perspective pouvant déformer les textures et entraîner l'apparition de flou. La raison à cela est que les techniques de filtrage de texture précédentes partent du principe que la texture est vue de face. Prenez une texture carrée, par exemple. Vue de face, elle ressemble à un carré sur l'écran. Mais tournez la caméra, de manière à voir la texture de biais, avec un angle, et vous verrez que la forme de la texture sur l'écran est un trapèze, pas un carré. Cette déformation liée à la perspective n'est pas prise en compte par les méthodes de filtrage de texture précédentes. Pour le dire autrement, les techniques de filtrage précédentes partent du principe que les 4 texels qui entourent un pixel forment un carré, ce qui est vrai si la texture est vue de face, sans angle, mais ne l'est pas si la texture n'est pas perpendiculaire à l'axe de la caméra. Du point de vue de la caméra, les 4 texels forment un trapèze d'autant moins proche d'un carré que l'angle est grand.
Pour corriger cela, les chercheurs ont inventé le '''filtrage anisotrope'''. En fait, je devrais plutôt dire : LES filtrages anisotropes. Il en existe un grand nombre, dont certains ne sont pas utilisés dans les cartes graphiques actuelles, soit car ils trop gourmand en accès mémoires et en calculs pour être efficaces, soit car ils ne sont pas pratiques à mettre en œuvre. Il est très difficile de savoir quelles sont les techniques de filtrage de texture utilisées par les cartes graphiques, qu'elles soient récentes ou anciennes. Beaucoup de ces technologies sont brevetées ou gardées secrètes, et il faudrait vraiment creuser les brevets déposés par les fabricants de GPU pour en savoir plus. Les algorithmes en question seraient de plus difficiles à comprendre, les méthodes mathématiques cachées derrière ces méthodes de filtrage n'étant pas des plus simple.
[[File:Anisotropic filtering en.png|centre|vignette|upright=2|Exemple de filtrage anisotrope.]]
==La compression de textures==
Les textures les plus grosses peuvent aller jusqu'au mébioctet, ce qui est beaucoup. Pour limiter la casse, les textures sont compressées. La '''compression de texture''' réduit la taille des textures, ce qui peut se faire avec ou sans perte de qualité. Elle entraîne souvent une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande. Mais il s'agit là d'une technique très simple, beaucoup plus simple que les techniques que nous allons voir dans cette section. Nous allons voir quelque algorithmes de compression de textures de complexité intermédiaire, mais n'allons pas voir l'état de l'art. Il existe des formats de texture plus récents que ceux qui nous allons aborder, comme l{{'}}''Ericsson Texture Compression'' ou l{{'}}''Adaptive Scalable Texture Compression'', plus complexes et plus efficaces.
Notons que les textures sont compressées dans les fichiers du jeu, mais aussi en mémoire vidéo. Les textures sont décompressées lors de la lecture. Pour cela, la carte graphique contient alors un circuit, capable de décompresser les textures lorsqu'on les lit en mémoire vidéo. Les cartes graphiques supportent un grand nombre de formats de textures, au niveau du circuit de décompression. Du fait que les textures sont décompressées à la volée, les techniques de compression utilisées sont assez particulières. La carte graphique ne peut pas décompresser une texture entière avant de pouvoir l'utiliser dans un ''pixel shader''. A la place, on doit pouvoir lire un morceau de texture, et le décompresser à la volée. On ne peut utiliser les méthodes de compression du JPEG, ou d'autres formats de compression d'image. Ces dernières ne permettent pas de décompresser une image morceau par morceau.
Pour permettre une décompression/compression à la volée, les textures sont des textures tilées, généralement découpées en tiles de 4 * 4 texels. Les ''tiles'' sont compressées indépendamment les unes des autres. Et surtout, avec ou sans compression, la position des tiles en mémoire ne change pas. On trouve toujours une tile tous les T octets, peu importe que la tile soit compressée ou non. Par contre, une tile compressée n'occupera pas T octets, mais moins, là où une tile compressée occupera la totalité des T octets. En clair, compresser une tile fait qu'il y a des vides entre deux tiles dans al mémoire vidéo, mais ne change rien à leur place en mémoire vidéo qui est prédéterminée, peu importe que la texture soit compressée ou non. L'intérêt de la compression de textures n'est pas de réduire la taille de la texture en mémoire vidéo, mais de réduire la quantité de données à lire/écrire en mémoire vidéo. Au lieu de lire T octets pour une tile non-compressée, on pourra en lire moins.
===La palette indicée et la technique de ''Vector quantization''===
La technique de compression des textures la plus simple est celle de la '''palette indicée''', que l'on a entraperçue dans le chapitre sur les cartes d'affichage. La technique de '''''vector quantization''''' peut être vue comme une amélioration de la palette, qui travaille non pas sur des texels, mais sur des ''tiles''. À l'intérieur de la carte graphique, on trouve une table qui stocke toutes les ''tiles'' possibles. Chaque ''tile'' se voit attribuer un numéro, et la texture sera composé d'une suite de ces numéros. Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l’embarqué utilisent ce genre de compression.
===Les algorithmes de ''Block Truncation coding''===
La première technique de compression élaborée est celle du '''''Block Truncation Coding''''', qui ne marche que pour les images en niveaux de gris. Le BTC ne mémorise que deux niveaux de gris par ''tile'', que nous appellerons couleur 1 et couleur 2, les deux niveaux de gris n'étant pas le même d'une ''tile'' à l'autre. Chaque pixel d'une ''tile'' est obligatoirement colorié avec un de ces niveaux de gris. Pour chaque pixel d'une ''tile'', on mémorise sa couleur avec un bit : 0 pour couleur 1, et 1 pour couleur 2. Chaque ''tile'' est donc codée par deux entiers, qui codent chacun un niveau de gris, et une suite de bits pour les pixels proprement dit. Le circuit de décompression est alors vraiment très simple, comme illustré ci-dessous.
[[File:Block Truncation coding.jpg|centre|vignette|upright=2.0|Block Truncation coding.]]
La technique du BTC peut être appliquée non pas du des niveaux de gris, mais pour chaque composante Rouge, Vert et Bleu. Dans ces conditions, chaque ''tile'' est séparée en trois sous-''tiles'' : un sous-bloc pour la composante verte, un autre pour le rouge, et un dernier pour le bleu. Cela prend donc trois fois plus de place en mémoire que le BTC pur, mais cela permet de gérer les images couleur.
===Le format de compression S3TC / DXTC===
L'algorithme de '''Color Cell Compression''', ou CCC, améliore le BTC pour qu'il gère des couleurs autre que des niveaux de gris. Ce CCC remplace les deux niveaux de gris par deux couleurs. Une ''tile'' est donc codée avec un entier 32 bits par couleur, et une suite de bits pour les pixels. Le circuit de décompression est identique à celui utilisé pour le BTC.
[[File:Color Cell Compression.jpg|centre|vignette|Color Cell Compression.]]
[[File:Dxt1-memory-layout.png|vignette|Dxt1 et ''color cell compression''.]]
Le format de compression de texture utilisé de base par Direct X, le DXTC, est une version amliorée de l'algorithme précédent. Il est décliné en plusieurs versions : DXTC1, DXTC2, etc. La première version du DXTC est une sorte d'amélioration du CCC : il ajoute une gestion minimale de transparence, et découpe la texture à compresser en ''tiles'' de 4 pixels de côté. La différence, c'est que la couleur finale d'un texel est un mélange des deux couleurs attribuée au bloc. Pour indiquer comment faire ce mélange, on trouve deux bits de contrôle par texel.
Si jamais la couleur 1 < couleur2, ces deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (2 * Couleur1 + Couleur2) / 3
* 11 = (Couleur1 + 2 * Couleur2) / 3
Sinon, les deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (Couleur1 + Couleur2) / 2
* 11 = Transparent
[[File:DXTC.jpg|centre|vignette|DXTC.]]
Le circuit de décompression du DXTC ressemble alors à ceci :
[[File:Circuit de décompression du DXTC.jpg|centre|vignette|upright=2.0|Circuit de décompression du DXTC.]]
===Les format DXTC 2, 3, 4 et 5 : l'ajout de la transparence===
Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3, lui-même replacé par le DXT4 et par le DXT5. Dans le DXT3, la transparence fait son apparition. Pour cela, on ajoute 64 bits par ''tile'' pour stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1.
[[File:Dxt23-memory-layout.png|centre|vignette|Dxt 2 et 3.]]
Dans le DXT4 et le DXT5, la méthode utilisée pour compresser les couleurs l'est aussi pour les valeurs de transparence. L'information de transparence est stockée par un en-tête contenant deux valeurs de transparence, le tout suivi d'une matrice qui attribue trois bits à chaque texel. En fonction de la valeur des trois bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1.
[[File:Dxt45-memory-layout.png|centre|vignette|Dxt 4 et 5.]]
===Le format de compression PVRTC===
Passons maintenant à un format de compression de texture un peu moins connu, mais pourtant omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle travaille surtout dans les cartes graphiques embarquées. Ses cartes se trouvent notamment dans l'ipad, l'iPhone, et bien d'autres smartphones actuels.
Avec le PVRTC, les textures sont encore une fois découpées en ''tiles'' de 4 texels par 4, mais la ressemblance avec le DXTC s’arrête là. Chacque ''tile'' est codée avec :
* une couleur codée sur 16 bits ;
* une couleur codée sur 15 bits ;
* 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
* et un bit de modulation, qui permet de configurer l’interprétation des bits de mélange.
Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4*4.
==Annexe : le ''normal-mapping'' hardware==
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, parlons un peu du ''normal mapping'' et de son implémentation dans les unités de texture. Pour rappel, 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. Et pour comprendre quel est le rapport avec les textures, nous allons devoir faire quelques rappels sur l'éclairage par pixel.
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
L'éclairage se fait en utilisant de nombreux calculs, qu'on a détaillé dans le chapitre sur les bases du rendu 3D. Ceux-ci utilisent la normale d'un sommet, à savoir un vecteur orienté à la verticale de la surface. En théorie, il y a une normale par sommet, ce qui fait que les calculs d'éclairage doivent se faire au niveau géométrique, avant la rastérisation. Mais pour obtenir un éclairage de meilleure qualité, il y a des techniques qui calcule l'éclairage d'une scène 3D pixel par pixel.
Une première technique d'éclairage par pixel interpole les normales lors de l'étape de rastérisation. On obtient alors un éclairage de Phong. Une autre solution est celle du ''normal mapping''. Le '''''normal mapping''''' précalcule les normales d'une surface dans une texture, appelée la ''normal map''. L'éclairage est alors réalisé par un pixel shader, qui lit les normales depuis cette texture et fait les calculs d'éclairage avec.
===Les ''normal maps'' sont des textures===
Notons que l'accès à la ''normal map'' se fait comme pour n'importe quelle texture : les texels sont lus en mémoire vidéo, puis le filtrage de texture est appliqué, et enfin le tout est envoyé au ''pixel shader''. En théorie, le filtrage effectue une sorte d'interpolation des normales automatique, ce qui permet de trouver la normale à un pixel précis, même s'il n'est pas sur un texel.
La ''normal map'' est une texture, donc. Mais ce n'est pas une texture comme une autre. Elle mémorise un vecteur pour chaque texel, pas une couleur. Il est possible d'utiliser les formats de textures vus plus haut, même si cela ne donne pas de bons résultats. Après tout, un vecteur est codé par trois coordonnées x,y,z, il est possible de coder chacune avec un octet et de packager cela dans une texture RGB classique. La coordonnée x va dans la composante Rouge, la coordonnée Y dans la composante Bleu, et la coordonnée z dans la composante z.
Le problème est que le rendu final n'est pas très beau. Et les opérations de filtrage ne donnent pas de très bons résultats. Pour remédier à ces problèmes, des formats de texture spécialisés pour les ''normal map'' ont été inventés. Le premier d'entre eux est le '''3Dc''', aussi appelé BC5 pour ''Block Compression 5''. Il utilise un octet par texel, au lieu de trois avec une texture RGB normale non-compressée.
===Le format de texture 3Dc===
Avec le 3Dc, les trois coordonnées des vecteurs ne sont pas mémorisées, seules deux d'entre elles le sont. En effet, les normales sont des vecteurs ''normalisés'', c'est à dire que leur longueur vaut 1, par construction. Vu que la taille est connue à l'avance, on peut en déduire la coordonnée z à partir des coordonnées x et y, avec l'usage du théorème de Pythagore. Par définition, <math>z^2 = 1 - x^2 - y^2</math>. Le calcul peut être fait dans le ''pixel shader'' sans problème.
Pour le reste, la compression de la ''normal map'' ressemble un peu à celle des textures. La ''normal map'' est découpée en blocs de 4 par 4 texels de côté, chacune étant encodée en tenant compte de certaines redondances. Dans un bloc, les coordonnées x sont dans un certain intervalle, allant de <math>x_{min}</math> à <math>x_{max}</math>, idem pour la coordonnée y qui est dans l'intervalle <math>y_{min}</math>, <math>y_{max}</math>. L'intervalle <math>x_{min}</math> à <math>x_{max}</math> est coupé en 8 parts égales, ce qui fait 8 valeurs possibles dans cet intervalle, chacune correspondant à une valeur x possible pour une normale du bloc. La normale est donc encodée en précisant quelle valeur est la bonne, en utilisant un indice de 3 bits pour cela. La valeur exacte de la coordonnée x se calcule à partir de l'indice comme suit :
: <math>x = x_{min} + \left( \frac{x_{min} - x_{min}}{8} \right) \times \text{indice} </math>
Pour encoder un bloc de 4 pr 4 normales, il faut donc :
* Un octet pour chaque coordonnée <math>x_{min}</math>, <math>x_{max}</math>, <math>y_{min}</math>, <math>y_{max}</math>.
* 6 bits par normale : 3 pour encoder le décalage de la coordonnée x, 3 pour la coordonnée y.
Le tout permet d'encoder un bloc de 16 normales en seulement 128 bits, soit un octet par normale. Le ''pixel shader'' se débrouille pour décompresser un bloc. Il effectue notamment le calcul du dessus, pour retrouver la coordonnée x.
==Annexe : les ''shadowmap'' hardware==
Les anciens GPU, notamment la Geforce FX, avaient des fonctionnalités spécifiques pour le calcul des ombres. Dans la plupart des jeux vidéos de l'époque, et même de maintenant, les ombres sont calculées avec la technique des ''shadowmap''. L'idée est assez simple sur le principe : un pixel est dans l'ombre quand il est invisible depuis une source de lumière.
L'idée est que le rendu est réalisé en plusieurs passes, avec une passe par source de lumière et une passe finale pour calculer l'image finale. Nous allons expliquer la technique avec une seule source de lumière, et allons utiliser l'exemple de la scène ci-dessous.
[[File:7fin.png|centre|vignette|Scène 3D d'exemple.]]
===La technique du ''shadowmapping''===
[[File:2shadowmap.png|vignette|Résultat de la première passe : ''shadowmap''..]]
La première passe rend l'image depuis le point de vue de la source de lumière. Cette première passe ne rend pas les couleurs de la scène, elle ne s'intéresse qu'à la profondeur des pixels. Le résultat est que l'image ne rend que le tampon de profondeur. Celui-ci est ensuite réutilisé comme texture pour la passe suivante. La texture en question est appelée la '''''shadownmap'''''.
La perspective utilisée, ainsi que le ''view frustrum'', dépend de la source de lumière. Pour une source de lumière qui émet un cône de lumière, le ''view frustrum'' de l'image rendue doit contenir tout le cone de lumière, et doit coller le plus possible à celui-ci. Pour une source directionnelle, comme le soleil, une perspective orthographique est utilisée.
La seconde passe rend l'image du point de vue de la caméra, pour rendre l'image finale. Elle rend l'image finale, qui est composée de pixels, chacun ayant une position à l'écran x,y, et une profondeur z. Les coordonnées sont transformées pour obtenir la position de ce pixel depuis le point de vue de la caméra. Une simple multiplication de matrice suffit, rien de bien compliqué, un shader peut le faire.
[[File:5failed.png|vignette|Résultat du test des comparaisons.]]
Après cette étape, on a alors les coordonnées x,y,z de ce pixel du point de vue de la caméra, et la ''shadowmap''. Il est alors possible d'accéder à la ''shadowmap'' au même endroit, à la même place que le pixel testé, aux mêmes coordonnées x,y. Si la profondeur du pixel est supérieure à celle de la shadowmap au même endroit, alors le pixel est situé derrière la surface visible, donc est dans l'ombre. Sinon, il n'est pas dans l'ombre. Le même procédé est répété sur chaque pixel de l'écran.
===Les optimisations hardware du ''shadowmapping''===
La technique des ''shadowmap'' demande donc de calculer une texture ''shadowmap'', puis de lire celle-ci et de faire des comparaisons de profondeur. Les GPU comme la Geforce FX intégraient du matériel dans les unités de texture pour faciliter ce travail. Les unités de texture pouvaient lire les ''shadowmap'', et faire la comparaison de profondeur toutes seules, elles avaient des circuits pour. Il suffisait de leur fournir le pixel à tester, ses coordonnées x,y,z, et l'adresse de la ''shadowmap''. Les unités de texture renvoyaient alors un résultat valant 0 ou 1 : 1 si le pixel est dans l'ombre, 0 sinon.
Elles pouvaient même effectuer du filtrage de texture sur les ''shadowmap''. Mais le filtrage était différent de celui utilisé sur les autres textures : moyenner des valeurs de profondeur ne marche pas bien. Elles utilisaient des techniques de filtrage différentes : elles faisaient les tests de comparaison, puis faisaient la moyenne des résultats. Ainsi, pour du filtrage bilinéaire, elles lisaient 4 texels dans la ''shadowmap'', puis faisaient 4 tests de comparaison, et moyennaient les 4 résultats.
==Annexe : les textures virtuelles==
Les '''textures virtuelles''' sont une optimisation des textures normales, qui visent à accélérer le rendu de terrains de grande taille. Imaginez par exemple un monde assez ouvert, comme un environnement en forêt ou en montagne, avec une grande distance de visibilité. Avec de tels terrains, le "sol" est recouvert par une texture de sol unique qui recouvre tout le terrain. Elle ne se répète pas, est de très grande taille, et peut parfois recouvrir toute la map ! Mais il n'y a pas assez de mémoire vidéo pour mémoriser la texture toute entière. La seule solution est la suivante : une partie de la texture est placée en mémoire vidéo, le reste est soit placé en mémoire RAM ou sur le disque dur.
Pour cela, le moteur de jeu utilise une optimisation ingénieuse, basée sur une observation assez basique : une bonne partie de la texture est visible, mais le reste est caché par des arbres, des habitations ou d'autres obstacles. Une optimisation possible de ne garder en mémoire vidéo que les portions visibles de la texture, pas les portions cachées. Une autre optimisation mélange textures virtuelles et ''mip-mapping''. L'idée est que pour les portions lointaines d'une texture, la texture utilisée est une ''mip-map'' de basse résolution. L'idée est alors de ne charger que la ''mip-map'' adéquate, pas les autres niveaux de détail. En clair, la texture de base n'est pas chargée en mémoire vidéo, mais la ''mip-map'' basse résolution l'est.
===Une texture à deux niveaux===
L'implémentation des textures virtuelles découpe les méga-textures en ''tiles'', en morceaux rectangulaires de taille modeste. En clair, le terrain est découpé en morceau rectangulaires/carrés. Seules les tiles nécessaires sont chargées en mémoire vidéo, pas les autres. Par exemple, les ''tiles'' non-visibles ne sont pas placées en mémoire vidéo, seules les ''tiles'' visibles le sont. De même, il y a une ''tile'' par niveau de mip-map : seul la tile correspondant le niveau adéquat est en mémoire vidéo, les autres niveaux de détail ne sont pas chargés. On peut faire une analogie avec la mémoire virtuelle, où les données sont découpées en pages, qui sont chargées en mémoire RAM à la demande, suivant les besoins, les données pouvant être swappées sur le disque dur si elles sont peu utilisées. Sauf qu'ici, il s'agit de textures qui sont découpées en pages chargées à la demande en mémoire vidéo, depuis la RAM système.
Une texture virtuelle est en réalité un système à deux niveaux : une liste de ''tiles'' et les ''tiles'' elles-mêmes. La liste de ''tiles'' est appelée un '''atlas de texture''', c'est un peu l'équivalent de la ''tilemap'' pour le rendu 2D. Rendre une texture demande de calculer quelle ''tile'' contient le texel à afficher, consulter la ''tile'' en question, puis récupérer le texel adéquat dans cette ''tile''. La ''tile'' est donc une texture, mais la texture à charger est choisie parmi un ensemble, qui est ici l'atlas de texture.
===L'implémentation : logicielle versus matérielle===
Les textures virtuelles ont été utilisées pour la première fois par les jeux Rage 1 et 2 d'IdSoftware, et quelques jeux ultérieurs comme DOOM 2016. IdSoftware les appelait des '''''mega-textures'''''. L'optimisation permettait des gains en performance assez impressionnants. Le jeu Rage 1 utilisait une texture carrée unique de 128k pixels de côté pour rendre le terrain. En théorie, une telle texture devrait prendre 64 giga-octets, mais le jeu tournait correctement avec 512 méga-octets de RAM, poussivement avec seulement 256 méga-octets de RAM.
De nos jours, les textures virtuelles sont supportées par beaucoup de jeux vidéos, les moteurs les plus courants gèrent de telles textures de manière logicielles. Mais quelques GPU récents supportent les textures virtuelles. Sur les GPU récents, l'atlas de texture est géré nativement par le matériel. Le GPU choisit quelle ''tile'', quelle texture choisir pour rendre le texel adéquat. Pour cela, le GPU calcule quelle ''tile'' charger, consulte l'atlas de texture, et lit la texture de ''tile'' adéquate.
Mais l'implémentation sur les GPU récents a de nombreuses limitations. La limitation la plus importante est que la taille des textures virtuelles ne peut pas dépasser la taille d'une texture normale, soit 32768 pixels de côté pour une texture carrée environ sur les GPU de 2020. De plus, le chargement d'une ''tile'' est très lent. En clair, dès qu'on veut changer de niveau de mip-map pour une tile, ou dès qu'une tile devient visible, le chargement de la tile peut facilement prendre plusieurs centaines de millisecondes. Le filtrage de texture est très complexe avec des textures virtuelles, ce qui fait que le filtrage de texture virtuelle est souvent soumis à des limitations que les textures normales n'ont pas, notamment pour le filtrage anisotropique.
{{NavChapitre | book=Les cartes graphiques
| prev=Le rasterizeur
| prevText=Le rasterizeur
| next=Les Render Output Target
| nextText=Les Render Output Target
}}{{autocat}}
8rr09m2efho38zarmxhhpla1bxqn0tf
763410
763409
2026-04-10T17:10:38Z
Mewtow
31375
/* Le format de texture 3Dc */
763410
wikitext
text/x-wiki
[[File:Texture mapping.png|vignette|''Texture mapping'']]
Les '''textures''' sont des images que l'on va plaquer sur la surface d'un objet, du papier peint en quelque sorte. Les cartes graphiques supportent divers formats de textures, qui indiquent comment les pixels de l'image sont stockés en mémoire : RGB, RGBA, niveaux de gris, etc. Une texture est donc composée de "pixels", comme toute image numérique. Pour bien faire la différence entre les pixels d'une texture, et les pixels de l'écran, les pixels d'une texture sont couramment appelés des ''texels''.
==Le placage de textures inverse==
Pour rappel, plaquer une texture sur un objet consiste à attribuer un texel à chaque sommet, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture.
Dans les faits, on n'utilise pas de coordonnées entières de ce type. Les coordonnées de texture sont deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. Le nom donnée à cette technique de description des coordonnées de texture s'appelle l''''''UV Mapping'''''.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Les API 3D modernes gèrent des textures en trois dimensions, ce qui ajoute une troisième coordonnée de texture notée w. Dans ce qui va suivre, nous allons passer les textures en trois dimensions sous silence. Elles ne sont pas très utilisées, la quasi-totalité des jeux vidéo et applications 3D utilisant des textures en deux dimensions. Par contre, le matériel doit gérer les textures 3D, ce qui le rend plus complexe que prévu. Il faut ajouter quelques circuits pour, de quoi gérer la troisième coordonnée de texture, etc.
Lors de la rastérisation, chaque fragment se voit attribuer un sommet, et donc la coordonnée de texture qui va avec. Si un pixel est situé pile sur un sommet, la coordonnée de texture de ce sommet est attribuée au pixel. Si ce n'est pas le cas, la coordonnée de texture finale est interpolée à partir des coordonnées des trois sommets du triangle rastérisé. L'interpolation en question a lieu dans l'étape de rastérisation, comme nous l'avons vu dans le chapitre précédent. Le fait qu'il y ait une interpolation fait que les coordonnées du pixel gagent à être des nombres flottants. On pourrait faire une interpolation avec des coordonnées de texture entières, mais les arrondis et autres imprécisions de calcul donneraient un résultat graphiquement pas terrible, et empêcheraient d'utiliser les techniques de filtrage de texture que nous verrons dans ce chapitre.
À partir de ces coordonnées de texture, la carte graphique calcule l'adresse du texel qui correspond, et se charge de le lire. Et toute la magie a lieu dans ce calcul d'adresse, qui part de coordonnées de texture flottante, pour arriver à une adresse mémoire. Le calcul de l'adresse du texel se fait en plusieurs étapes, que nous allons voir ci-dessous. La première étape convertit les coordonnées flottantes en coordonnées entières, qui disent à quel ligne et colonne se trouve le texel voulu dans la texture. L'étape suivante transforme ces coordonnées x,y entières en adresse mémoire.
===La normalisation des coordonnées===
J'ai dit plus haut que les coordonnées de texture sont des coordonnées flottantes, comprises entre 0 et 1. Mais il faut savoir que les pixels shaders peuvent modifier celles-ci pour mettre en œuvre certains effets graphiques. Et le résultat peut alors se retrouver en-dehors de l'intervalle 0,1. C'est quelque chose de voulu et qui est traité par la carte graphique automatiquement, sans que ce soit une erreur. Au contraire, la manière dont la carte graphique traite cette situation permet d'implémenter des effets graphiques comme des textures en damier ou en miroir.
[[File:Clamp tile.jpg|vignette|Clamp tile]]
Il existe globalement trois méthodes très simples pour gérer cette situation, qui sont appelés des '''modes d'adressage de texture'''.
* La première méthode est de faire en sorte que le résultat sature. Si une coordonnée est inférieur à 0, alors on la remplace par un zéro. Si elle est supérieure à 1, on la ramène à 1. Avec cette méthode, tout se passe comme si les bords de la texture étaient étendus et remplissaient tout l'espace autour de la texture. Le tout est illustré ci-dessous. Ce mode d'accès aux textures est appelé le '''''clamp'''''.
* Une autre solution retire la partie entière de la coordonnée, elle coupe tout ce qui dépasse 1. Pour le dire autrement, elle calcule le résultat modulo 1 de la coordonnée. Le résultat est que tout se passe comme si la texture était répétée à l'infini et qu'elle pavait le plan.
* Une autre méthode remplit les coordonnées qui sortent de l’intervalle 0,1 avec une couleur préétablie, configurée par le programmeur.
===La conversion des coordonnées de textures flottantes en adresse mémoire===
Une fois la normalisation effectuée, les coordonnées de texture sont utilisées pour lire le texel voulu. Pour cela, les coordonnées de texte sont transformées en adresse mémoire, adresse qui pointe sur le texel ayant ces cordonnées. Pour cela, la première étape est de transformer les coordonnées flottantes u,v en coordonnées entières x,y qui pointent sur un texel. Pour cela, il suffit de multiplier les coordonnées flottantes u,v par la résolution de la texture accédée. Pour un écran de résolution <math>\text{height,width}</math>, le calcul est le suivant :
: <math>x = u \times \text{width}</math>
: <math>y = v \times \text{height}</math>
Le résultat est un nombre avec une partie entière et une partie fractionnaire. La partie entière des deux coordonnées donne la position x,y voulue, et la partie fractionnaire est conservée pour le filtrage de textures, mais passons cela sous silence pour le moment.
La seconde étape prend les coordonnées entières x,y et calcule l'adresse mémoire du texel. L'adresse dépend de la position de la texture en mémoire, précisément de son début, son premier texel, mais aussi de la position du texel par rapport au début de la texture. Et calculer cette position intra-texture dépend de la manière dont les texels sont stockés en mémoire.
====Les textures naïves====
Les programmeurs qui lisent ce cours s'attendent certainement à ce que la texture soit stockée en mémoire ligne par ligne, ou colonne par colonne. Cela veut dire que le premier pixel en partant d'en haut à gauche est stocké en premier, puis celui immédiatement à sa droite, puis celui encore à droite, et ainsi de suite. Une fois qu'on arrive à la fin d'une ligne, on passe à la ligne suivante, en-dessous. Cette organisation ligne par ligne s'appele l'organisation '''''row major order'''''. On peut faire pareil, mais colonne par colonne, ce qui donne le '''''column major order'''''.
[[File:Speicheranordnung Feld.svg|centre|vignette|upright=2|Row et column major order.]]
Maintenant, supposons que la texture commence à l'adresse <math>A_\text{texture}</math>, qui est l'adresse du premier texel. La texture a une résolution de <math>\text{width}</math> texels de large et <math>\text{height}</math> texels de haut. Par définition, les coordonnées X et Y des texels commencent à 0, ce qui fait que le pixel en haut à gauche a les coordonnées 0,0.
L'adresse du pixel se calcule comme suit :
: <math>A_\text{pixel} = A_\text{texture} + (\text{taille d'une ligne en octets} \times Y) + (\text{taille d'un texel en octets} \times X)</math>
La taille d'un pixel en mémoire est notée T. La taille d'une ligne en mémoire est de <math>width \times T</math>, par définition, vu qu'elle fait <math>width</math> texels. On a donc :
: <math>A_\text{pixel} = A_\text{texture} + (width \times T \times Y) + (T \times X)</math>
La formule se réécrit comme suit :
: <math>A_\text{pixel} = A_\text{texture} + T \times (width \times Y + X)</math>
Le calcul d'adresse est donc assez simple. Malheureusement, les textures ne sont pas stockées de cette manière en mémoire vidéo. En effet, elle se marie mal avec les opérations de filtrage de texture que nous allons voir dans ce qui suit. Le filtrage d'un texel dépend de ses voisins du dessus et du dessous. Le fait que la texture n'est pas forcément parcourue ligne par ligne fait que stocker une texture ligne par ligne n'est pas l'idéal.
De même, les textures sont déformées par la perspective. L'affichage de la texture ne se fait alors pas ligne par ligne, mais en parcourant la texture en diagonale, l'angle de la diagonale correspondant approximativement à l'angle que fait la verticale de la texture avec le regard. Vu qu'on ne connait pas à l'avance l'angle que fera la diagonale de parcours, on doit ruser.
====Les textures tilées====
Une première solution à ce problème est celle des '''textures tilées'''. Avec ces textures, l'image de la texture est découpée en ''tiles'', des rectangles ou en carrés de taille fixe, généralement des carrés de 4 pixels de côté. Les tiles ont une largeur et une longueur égales, afin de simplifier les calculs : on divise X et Y par le même nombre. De plus, leur largeur et leur longueur sont une puissance de deux, afin de simplifier les calculs d'adresse. Les ''tiles'' sont alors mémorisée les unes après les autres dans le fichier de la texture.
[[File:Texture tilée.png|centre|vignette|upright=2|Texture tilée]]
La formule de calcul d'adresse vue plus haut doit être adaptée pour tenir compte des tiles. Pour cela, il faut remplacer la taille d'un texel par la taille d'une tile, et que la largeur de la texture soit exprimée en nombre de tiles. De plus, on doit adapter les coordonnées des texels pour donner des coordonnées de tile. Généralement, les tiles sont des carrés de N pixels de côté, ce qui fait qu'on peut regrouper les lignes et les colonnes par paquets de N. Il suffit donc de diviser Y et X pour obtenir les coordonnées de la tile, de même que la larguer. La formule pour calculer la position de la énième tile est alors la suivante :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + \text{Taille mémoire d'une tile} \times \left( {\text{Width} \over N} \times {Y \over N} + {X \over N} \right)</math>
On peut réécrire le tout comme suit :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + K \times \left( {Y \over N} + X \right)</math>, avec K une constante connue à la compilation des shaders.
Vu que les tiles sont carrées avec une largeur qui est une puissance de deux, la multiplication par la taille d'une tile en mémoire se simplifie : on passe d'une multiplication entière à des décalages de bits. Même chose pour le calcul de l'adresse de la tile à partir des coordonnées x,y : ils impliquent des divisions par une puissance de deux, qui deviennent de simples décalages.
La position d'un pixel dans une tile dépend du format de la texture, mais peut se calculer avec quelques calculs arithmétiques simples. Dans les cas les plus simples, les pixels sont mémorisés ligne par ligne, ou colonne par colonne. Mais ce n'est pas systématiquement le cas. Toujours est-il que les calculs pour déterminer l'adresse sont simples, et ne demandent que quelques additions ou multiplications. Mais avec les formats de texture utilisés actuellement, les tiles sont chargées en entier dans le cache de texture, sans compter que diverses techniques de compression viennent mettre le bazar, comme on le verra dans la suite de cours.
Un avantage de l'organisation en tiles est qu'elle se marie bien avec le parcours des textures. On peut parcourir une texture dans tous les sens, horizontal, vertical, ou diagonal, on sait que les prochains pixels ont de fortes chances d'être dans la même tile. Si on rentre dans une tile par la gauche en haut, on a encore quelques pixels à parcourir dans la tile, par exemple. De même, le filtrage de textures est facilité. On verra dans ce qui va suivre que le filtrage de texture a besoin de lire des blocs de 4 texels, des carrés de 2 pixels de côté. Avec l'organisation en tile, on est certain que les 4 texels seront dans la même tile, sauf s'ils ont le malheur d'être tout au bord d'une tile. Ce dernier cas est assez rare, et il l'est d'autant plus que les tiles sont grandes. Enfin, un dernier avantage est que les tiles sont généralement assez petites pour tenir tout entier dans une ligne de cache. Le cache de texture est donc utilisé à merveille, ce qui rend les accès aux textures plus rapides.
====Les textures basées sur des ''z-order curves''====
Les formats de textures théoriquement optimaux utilisent une '''''Z-order curve''''', illustrée ci-dessous. L'idée est de découper la texture en quatre rectangles identiques, et de stocker ceux-ci les uns à la suite des autres. L'intérieur de ces rectangles est lui aussi découpé en quatre rectangles, et ainsi de suite. Au final, l'ordre des pixels en mémoire est celui illustré ci-dessous.
[[File:Z-CURVE.svg|centre|vignette|upright=2|Construction d'une ''Z-order curve''.]]
Les texels sont stockés les uns à la suite des autres dans la mémoire, en suivant l'ordre donnée par la ''Z-order curve''. Le calcul d'adresse calcule la position du texel en mémoire, par rapport au début de la texture, et ajoute l'adresse du début de la texture. Mais tout le défi est de calculer la position d'un texel en mémoire, à partir des coordonnées x,y. Le calcul peut sembler très compliqué, mais il n'en est rien. Le calcul demande juste de regarder les bits des deux coordonnées et de les combiner d'une manière particulièrement simple. Il suffit de placer le bit de poids fort de la coordonnée x, suivi de celui de la coordonnée y, et de faire ainsi de suite en passant aux bits suivants.
[[File:Zcurve45bits.png|centre|vignette|upright=1.5|Calcul de la position d'un élément dans une ''Z-order curve'' à partir des coordonnées x et y.]]
L'avantage d'une telle organisation est que la textures est découpées en ''tiles'' rectangulaires d'une certaine taille, elles-mêmes découpées en ''tiles'' plus petites, etc. Et il se trouve que cette organisation est parfaite pour le cache de texture. L'idéal pour le cache de texture est de charger une ''tile'' complète dans le cache de textures. Quand on accède à un texel, on s'assure que la ''tile'' complète soit chargée. Mais cela demande de connaitre à l'avance la taille d'une ''tile''. Les formats de texture fournissent généralement une ''tile'' carré de 4 pixels de côté, mais cela donnerait un cache trop petit pour être vraiment utile. Avec cette méthode, on s'assure qu'il y ait une ''tile'' avec la taille optimale. Les ''tiles'' étant découpées en ''tiles'' plus petites, elles-mêmes découpées, et ainsi de suite, on s'assure que la texture est découpées en ''tiles'' de taille variées. Il y aura au moins une ''tile'' qui rentrera tout pile dans le cache.
==Les techniques de rendu à textures multiples==
Nous venons de voir comment une texture est plaquée sur un objet 3D, ou une surface comme un sol. Pour résumer, le calcul de l'adresse d'un texel prend la position du texel par rapport au début de la texture, et ajoute l'adresse du début de la texture. L'adresse mémoire de la texture est connue au moment où le pilote de la carte graphique place la texture dans la mémoire vidéo, et cette information est transmise au matériel par l'intermédiaire du processeur de commande, puis passée aux processeurs de shaders et à l'unité de texture. Le tout est couplé à d'autres informations, la plus importante étant la ''taille de la texture en octets'', pour éviter de déborder lors des accès à la texture.
Néanmoins, il s'agit là du cas le plus simple. Certaines techniques de rendu demandent de choisir la texture à plaquer parmi un ensemble de plusieurs textures. Les techniques en question sont assez variées et n'ont pas grand chose en commun. Les plus connues sont le ''mip-mapping'', le ''cube-mapping'' et les textures virtuelles. Le ''mip-mapping'' sert à filtrer les textures, chose qu'on expliquera plus tard, le ''cube-mapping'' sert à simuler des réflexions sur un objet en plaquant une texture de l'environnement dessus, les textures virtuelles sont une optimisation pour les textures des terrains de grande taille. Mais malgré leurs différences, elles demandent de choisir quelle texture plaquer entre plusieurs textures de base. En clair, l'adresse de base de la texture varie selon la situation. Voyons-les dans le détail.
===Le mip-mapping===
Le '''mip-mapping''' a pour but de légèrement améliorer les graphismes des objets lointains, tout en rendant les calculs de texture plus rapides. Formellement, le ''mip-mapping'' est une technique de filtrage de texture, mais nous l'abordons maintenant car elle est surtout liée au calcul d'adresse. Les unités de texture ont des circuits de filtrage de texture séparés des circuits de ''mip-mapping'' et de calcul d'adresse, d'où le fait que nous en parlons séparément.
Le problème résolu par le ''mip-mapping'' est le rendu des textures lointaines. Si une texture est plaquée sur un objet lointain, une bonne partie des détails est invisible pour l'utilisateur. Un pixel de l'écran est associé à plusieurs texels. Idéalement, la carte graphiques devrait lire tous ces texels et en faire une sorte de moyenne pondérée, pour calculer la couleur finale du pixel. Mais dans les faits, ce serait très gourmand et compliqué à implémenter en hardware. Une solution serait de ne garder que quelque texels, mais cela a tendance à créer des artefacts visuels (les textures affichées ont tendance à pixeliser). Le ''mip-mapping'' permet de réduire ces deux problèmes en même temps en précalculant cette moyenne pondérée pour des distances prédéfinies.
L'idée est d'utiliser plusieurs exemplaires d'une même texture à des résolutions différentes, chaque exemplaire étant adapté à une certaine distance. Par exemple, une texture sera stocké avec un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre de 128 * 128 et ainsi de suite jusqu’à un dernier exemplaire de 32 * 32 pixel. Chaque exemplaire correspond à un '''niveau de détail''', aussi appelé ''Level Of Detail'' (abrévié en LOD). La résolution utilisée diminue d'autant plus que l'objet est situé loin de la caméra. Les objets proches seront rendus avec la texture 512*512, ceux plus lointains seront rendus avec la texture de résolution 256*256, les textures 128*128 seront utilisées encore plus loin, et ainsi de suite jusqu'aux objets les plus lointains qui sont rendus avec la texture la plus petite de 32*32.
[[File:MipMap Example STS101.jpg|centre|vignette|upright=2|Exemples de mip-maps.]]
Le ''mip-mapping'' améliore grandement la qualité d'image. L'image d'exemple ci-dessous le montre assez bien.
[[File:Mipmapping example.png|centre|vignette|upright=2|Exemple de mipmapping.]]
Pour faciliter les calculs d'adresse, les LOD d'une même texture sont stockées les uns après les autres en mémoire (dans un tableau, comme diraient les programmeurs). Ainsi, pas besoin de se souvenir de la position en mémoire de chaque LOD : l'adresse de la texture de base, et quelques astuces arithmétiques suffisent. Prenons le cas où la texture de base a une taille L. le premier exemplaire est à l'adresse 0, le second niveau de détail est à l'adresse L, le troisième à l'adresse L + L/4, le suivant à l'adresse L + L/4 + L/16, et ainsi de suite. Le calcul d'adresse demande juste connaître le niveau de détails souhaité et l'adresse de base de la texture. Le niveau de détail voulu est calculé par les pixel shaders, en fonction de la coordonnée de profondeur du pixel à traiter.
Évidemment, cette technique consomme de la mémoire vidéo, vu que chaque texture est dupliquée en plusieurs exemplaires, en plusieurs LOD. Dans le détail, la technique du mip-mapping prend au maximum 33% de mémoire en plus (sans compression). Cela vient du fait qu'en prenant une texture dexu fois plus petite, elle prend 4 fois moins de mémoire : 2 fois moins de pixels en largeur, et 2 fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des LODs, texture de base comprise, prendra X + (X/4) + (X/16) + (X/256) + … Un petit calcul de limite donne 4/3 * X, soit 33% de plus.
===Le cube-mapping===
[[File:Cube mapped reflection example 2.JPG|vignette|Exemple de reflets environnementaux.]]
L''''environnement-mapping''' est une technique de calcul de divers effets graphiques liés à l'environnement, notamment des réflexions. L'idée est de plaquer une texture pré-calculée pour simuler l'effet de l'environnement sur une surface ou un objet 3D. Il en existe plusieurs versions différentes, mais la seule utilisée de nos jours est le ''cube-mapping'', où la texture de l'environnement est plaquée sur un cube, d'où son nom. Le cube en question est utilisé différemment suivant ce que l'on cherche à faire avec le ''cube-mapping''. Les deux utilisations principales sont le rendu du ciel et des décors, et les réflexions sur la surface des objets. Dans les deux cas, l'idée est de précalculer ce que l'on voit du point de vue de la caméra. On place la caméra dans la scène 3D, on place un cube centré sur la caméra, le cube est texturé avec ce que l'on voit de l'environnement depuis la caméra/l'objet de son point de vue.
[[File:Panorama cube map.png|centre|vignette|upright=2|L'illustration montre en premier lieu une ''cubemap'' avec les six faces mises en évidence, puis quel environnement 3D elle permet de simuler, le troisième illustration montrant comment la ''cubemap'' est utilisée pour simuler l'environnement.]]
Le rendu du ciel et des décors lointains dans les jeux vidéo se base sur des '''''skybox''''', à savoir un cube centré sur la caméra, sur lequel on ajoute des textures de ciel ou de décors lointains. Le cube est recouvert par une texture, qui correspond à ce que l'on voit quand on dirige le regard de la caméra vers cette face. Contrairement à ce qu'on pourrait croire, la skybox n'est pas les limites de la scène 3D, les limites du niveau d'un jeu vidéo ou quoique ce soit d'autre de lié à la physique de la scène 3D. La skybox est centrée sur la caméra, elle suit la caméra dans son mouvement. Centrer la skybox sur la caméra permet de simuler des décors très lointains, suffisamment lointain pour qu'on n'ait pas l'illusion de s'en rapprocher en se déplaçant dans la map. De plus, cela évite d'avoir à faire trop de calculs à chaque fois que l'on bouge la caméra. La texture plaquée sur le cube est une texture unique, elle-même découpée en six sous-textures, une par face du cube.
[[File:Skybox example.png|centre|vignette|upright=2|Exemple de Skybox.]]
[[File:Cube mapped reflection example.jpg|vignette|Réflexions calculées par une ''cubemap''.]]
Le ''cube-mapping'' est aussi utilisé pour des reflets. L'idée est de simuler les reflets en plaquant une texture pré-calculée sur l'objet réflecteur. La texture pré-calculée est un dessin de l'environnement qui se reflète sur l'objet, un dessin du reflet à afficher. En la plaquant la texture sur l'objet, on simule ainsi des reflets de l'environnement, mais on ne peut pas calculer d'autres reflets comme les reflets objets mobiles comme les personnages. Et il se trouve que la texture pré-calculée est une ''cubemap''. Pour les environnements ouverts, c'est la ''skybox'' qui est utilisée, ce qui permet de simuler les reflets dans les flaques d'eau ou dans des lacs/océans/autres. Pour les environnements intérieurs, c'est une cubemap spécifique qui utilisée. Par exemple, pour l'intérieur d'une maison, on a une ''cubemap'' par pièce de la maison. Les reflets se calculent en précisant quelle ''cubemap'' appliquer sur l'objet en fonction de la direction du regard.
[[File:Cube map level.png|centre|vignette|Cube map de l'intérieur d'une pièce d'un niveau de jeux vidéo.]]
Toujours est-il que les textures utilisées pour le ''cubemmapping'', appelées des ''cubemaps'', sont en réalité la concaténation de six textures différentes. En mémoire vidéo, la ''cubemap'' est stockée comme six textures les unes à la suite des autres. Lors du rendu, on doit préciser quelle face du cube utiliser, ce qui fait 6 possibilités. On a le même problème qu'avec les niveaux de détail, sauf que ce sont les faces d'une ''cubemap'' qui remplacent les textures de niveaux de détails. L'accès en mémoire doit donc préciser quelle portion de la ''cubemap'' il faut accéder. Et l'accès mémoire se complexifie donc. Surtout que l'accès en question varie beaucoup suivant l'API graphique utilisée, et donc suivant la carte graphique.
Les API 3D assez anciennes ne gérent pas nativement les ''cubemaps'', qui doivent être émulées en logiciel en utilisant six textures différentes. Le pixel shader décide donc quelle ''cubemap'' utiliser, avec quelques calculs sur la direction du regard. L'accès se fait d'une manière assez simple : le shader choisit quelle texture utiliser. Les API 3D récentes gèrent nativement les ''cubemaps''. Dans le cas le plus simple,pour les versions les plus vielles de ces API, les six faces sont numérotées et l'accès à une ''cubemap'' précise quel face utiliser en donnant son numéro. La carte graphique choisit alors automatiquement la bonne texture, mais cela demande de laisser le calcul de la bonne face au pixel shader. D'autres API 3D et cartes graphiques font autrement. Dans les API 3D modenres, les ''cubemap'' sont gérées comme des textures en trois dimensions, adressées avec trois coordonnées u,v,w. La carte graphique utilise ces trois coordonnées de manière à en déduire quelle est la face pertinente, mais aussi les coordonnées u,v dans la texture de la face.
==L'implémentation matérielle du placage de textures==
Pour résumer, la lecture d'un texel demande d'effectuer plusieurs étapes. Dans le cas le plus simple, sans ''mip-mapping'' ou ''cubemapping'', on doit effectuer les étapes suivantes :
* Il faut d'abord normaliser les coordonnées de texture pour qu'elles tombent dans l'intervalle [0,1] en fonction du mode d'adressage désiré.
* Ensuite, les coordonnées u,v doivent être converties en coordonnées entières, ce qui demande une multiplication flottante.
* Enfin, l'adresse finale est calculée à partir des coordonnées entières et en ajoutant l'adresse de base de la texture (et éventuellement avec d'autres calculs arithmétiques suivant le format de la texture).
Tout cela pourrait être fait par le pixel shaders, mais cela implique beaucoup de calculs répétitifs et d'opérations arithmétiques assez lourdes, avec des multiplications flottantes, des additions et des multiplications entières, etc. Faire faire tous ces calculs par les shaders serait couteux en performance, sans compter que les shaders deviendraient plus gros et que cela aurait des conséquences sur le cache d'instruction. De plus, certaines de ces étapes peuvent se faire en parallèle, comme les deux premières, ce qui colle mal avec l'aspect sériel des shaders.
Aussi, les processeurs de shaders incorporent une unité de calcul d'adresse spéciale pour faire ces calculs directement en matériel. L'unité de texture contient au minimum deux circuits : un circuit de calcul d'adresse, et un circuit d'accès à la mémoire. Toute la difficulté tient dans le calcul d'adresse, plus que dans le circuit de lecture. Le calcul d'adresse est conceptuellement réalisé en deux étapes. La première étape qui transforme les coordonnées u,v en coordonnées x,y qui donne le numéro de la ligne et de la colonne du texel dans la texture. La seconde étape prend ces deux coordonnées x,y, l'adresse de la texture, et détermine l'adresse de la tile à lire.
[[File:Unité de texture simple.png|centre|vignette|upright=2|Unité de texture simple]]
===L'implémentation du mip-mapping===
Le ''mip-mapping'' est lui aussi pris en charge par l'unité de calcul d'adresse, car cette technique change l'adresse de base de la texture. La gestion du ''mip-mapping'' est cependant assez complexe. Il est possible de laisser le pixel shader calculer quel niveau de détail utiliser, en fonction de la coordonnée de profondeur z du pixel à afficher. La carte graphique détermine alors automatiquement quelle texture lire, quel niveau de détail, automatiquement. Elle détermine aussi la bonne résolution pour la texture, qui est égal à la résolution de la texture de base, divisée par le niveau de détail. Pour résumer, le niveau de détail est envoyé aux unités de texture, qui s'occupent de calculer l'adresse de base et la résolution adéquates. Quelques calculs arithmétiques simples, donc, qui s'implémentent facilement avec quelques circuits.
Mais une autre méthode laisse la carte graphique déterminer le niveau de détail par elle-même. Dans ce cas, cela demande, outre les deux coordonnées de texture, de calculer la dérivée de ces deux coordonnées dans le sens horizontal et vertical, ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Les quatre dérivées sont les suivantes :
: <math>\frac{du}{dx}</math>, <math>\frac{dv}{dx}</math>, <math>\frac{du}{dy}</math>, <math>\frac{dv}{dy}</math>
Un bon moyen pour obtenir les dérivées demande de regrouper les pixels par groupes de 4 et de faire la différence entre leurs coordonnées de texture respectives. On peut calculer les deux dérivées horizontales en comparant les deux pixels sur la même ligne, et les deux dérivées verticales en comparant les deux pixels sur la même colonne. Mais cela demande de rastériser les pixels par groupes de 4, par ''quads''. Et c'est ce qui est fait sur les cartes graphiques actuelles, qui rastérisent des groupes de 4 pixels à la fois.
[[File:Texture sampler unit with mipmapping.png|centre|vignette|upright=2.0|Unité de texture avec mipmapping.]]
Malheureusement, le calcul exact utilisé pour le choix de la mip-map dépend du GPU considéré et peu de chose est connu quant à ces algorithmes. Il est possible d'inférer le comportement à partir d'observations, mais guère plus. Pour ceux qui veulent en savoir plus, je conseille la lecture de cet article de blog :
* [https://pema.dev/2025/05/09/mipmaps-too-much-detail/ Mipmap selection in too much detail]
===La gestion des accès mémoire===
Enfin, l'unité de texture doit tenir compte du fait que la mémoire vidéo met du temps à lire une texture. En théorie, l'unité de texture ne devrait pas accepter de nouvelle demande de lecture tant que celle en cours n'est pas terminée. Mais faire ainsi demanderait de bloquer tout le pipeline, de l'''input assembler'' au unités de''shaders'', ce qui est tout sauf pratique et nuirait grandement aux performances.
Une solution alternative consiste à mettre en attente les demandes de lectures de texture pendant que la mémoire est occupée. La manière la plus simple d'implémenter des accès mémoire multiples est de les mettre en attente dans une petite mémoire FIFO. Cela implique que les accès mémoire s’exécutent dans l'ordre demandé par le ''shader'' et/ou l'unité de rastérisation, il n'y a pas de réorganisation des accès mémoire ou d’exécution dans le désordre des accès mémoire.
[[File:Texture prefetching.png|centre|vignette|upright=1.5|Accès mémoire simultanés.]]
Évidemment, quand la mémoire FIFO est pleine, le pipeline est alors totalement bloqué. Le rasteriser est prévenu que l'unité de texture ne peut pas accepter de nouvelle lecture de texture. En pratique, la FIFO est généralement d'une taille respectable et permet de mettre en attente beaucoup de demandes de lecture de texture. Il faut de plus noter qu'il y a une FIFO par processeur de ''shader'' sur les cartes graphiques modernes. Quand elle est pleine, le processeur cesse d'exécuter de nouveaux accès mémoire, mais peut continuer à exécuter des ''shaders'' dans les autres unités de calcul, pas besoin de bloquer complétement le pipeline.
===L'intégration du cache de textures===
Il faut noter que les unités de texture incorporent aussi un cache de texture, voire plusieurs. L'intégration des caches de texture avec la mémoire FIFO précédente est quelque peu compliqué, car il faut garantir que les lectures de texture se fassent dans le bon ordre. On ne peut pas exécuter une lecture dans le cache alors que des lectures précédentes sont en attente de lecture en mémoire vidéo. Et cela pose un gros problème : une lecture dans le cache de texture prend quelques dizaines de cycles d'horloge, alors qu'une lecture en mémoire vidéo en prend facilement 400 à 800 cycles, parfois plus. Et cela fait que l'ordre des accès mémoire peut s'inverser.
Prenons par exemple un accès au cache précédé et suivi par deux accès en mémoire vidéo. Le premier démarre au cycle 1, et se termine au cycle numéro 400. L'accès au cache commence au cycle 2 et se termine 20 cycles après, au cycle numéro 22. En clair, la lecture dans le cache s'est terminée avant l'accès mémoire qui le précède. Les textures ne sont donc plus lues dans l'ordre. Et il faut trouver une solution pour éviter cela.
La solution est de retarder les lectures dans le cache tant que tous les accès précédents ne sont pas terminés. Mais pour retarder les lectures en question, il faut d'abord savoir si la lecture atterrit dans le cache ou non, ce qui demande d'accéder au cache. On fait face à un dilemme : on veut retarder les accès au cache, mais les différencier des lectures déclenchant des accès mémoire demande d'accéder au cache en premier lieu. La solution est décrite dans l'article "Prefetching in a Texture Cache Architecture" par Igehy et ses collègues. Elle se base sur deux idées combinées ensemble.
La première idée est de séparer l'accès au cache en deux : une étape qui vérifie si les texels à lire sont dans le cache, et une étape qui accède aux données dans le cache lui-même. Un cache de texture est donc composé de deux circuits principaux. Le premier vérifie la présence des texels dans le cache. Il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Ensuite, en plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
La seconde idée est de retarder l'accès au cache entre les deux phases. La première étape d'un accès mémoire vérifie si la donnée est dans le cache ou non. Puis, on retarde la lecture des données, pour attendre que toutes les lectures précédentes soient terminées. Et enfin, troisième étape : la lecture des texels dans la mémoire cache proprement dite. Les accès mémoire passant par la mémoire vidéo se font de la même manière, à une différence près : la lecture dans le cache est remplacée par la lecture en mémoire vidéo. Tout démarre avec une demande à l'unité de tags, qui vérifie si le texel est dans le cache ou non. Puis on retarde l'accès tant que la mémoire vidéo est occupée, puis on effectue la lecture en mémoire vidéo.
Si ce n'est pas le cas, l'accès mémoire est envoyé à la mémoire vidéo comme précédemment, à savoir qu'il est mis en attente dans une mémoire FIFO, puis envoyé à la mémoire vidéo dès que celle-ci est libre. Mais en sortie de la mémoire, la donnée lue est envoyée dans le cache de texture, par dans l'unité de filtrage. Pour savoir où placer la donnée lue, l'unité de tag a réservé une ligne de cache précise, une adresse bien précise. L'adresse en question est disponible en lisant une autre mémoire FIFO, qui a mis en attente l'adresse en question, en attendant que l'accès mémoire se termine. La donnée est alors écrite dans le cache, puis lue par l'unité de filtrage de textures.
Pour une lecture dans le cache, le déroulement est similaire, mais sans le passage par la mémoire. La lecture fait une demande à l'unité de tag, et celle-ci répond que la donnée est bien dans le cache. Elle place alors l'adresse à lire dans la file d'attente. Une fois que les accès mémoire précédents sont terminés, l'adresse sort de la file d'attente et est envoyée à la mémoire de données. La lecture s'effectue, les texels sont envoyés à l'unité de filtrage de textures. La seule différence avec un ''phased cache'' normal est l'insertion de l'adresse à lire dans une FIFO qui vise à mettre en attente
[[File:Unité de texture avec un cache de texture.png|centre|vignette|upright=2.0|Unité de texture avec un cache de texture]]
Pour résumer, l'implémentation précédente garantit une exécution des lectures dans leur ordre d'arrivée. Et pour cela, elle retarde les lectures dans le cache tant que les lectures en mémoire précédentes ne sont pas terminées. L'accès au cache est plus rapide que l'accès en mémoire vidéo, mais le retard ajouté pour garantir l'ordre des lectures fait que le temps d'accès est très long.
==Le filtrage de textures==
Plaquer des textures sans autre forme de procès ne suffit pas à garantir des graphismes d'une qualité époustouflante. La raison est que les sommets et les texels ne tombent pas tout pile sur un pixel de l'écran : le sommet associé au texel peut être un petit peu trop en haut, ou trop à gauche, etc. Une explication plus concrète fait intervenir les coordonnées de texture. Souvenez-vous que lorsque l'on traduit une coordonnée de texture u,v en coordonnées x,y, on obtient un résultat qui ne tombe pas forcément juste. Souvent, le résultat a une partie fractionnaire. Si celle-ci est non-nulle, cela signifie que le texel/sommet n'est pas situé exactement sur le pixel voulu et que celui-ci est situé à une certaine distance. Concrètement, le pixel tombe entre quatre texels, comme indiqué ci-dessous.
[[File:Filtrage texture.png|centre|vignette|upright=2.0|Position du pixel par rapport aux texels.]]
Pour résoudre ce problème, on doit utiliser différentes techniques d'interpolation, aussi appelées techniques de '''filtrage de texture''', qui visent à calculer la couleur du pixel final en fonction des texels qui l'entourent. Il existe de nombreux types de filtrage de textures, qu'il s'agisse du filtrage linéaire, bilinéaire, trilinéaire, anisotropique et bien d'autres.
Tous ont besoin d'avoir certaines informations qui sont généralement fournies par les circuits de calcul d'adresse. La première est clairement la partie fractionnaire des coordonnées x,y. La seconde est la dérivée de ces deux coordonnées dans le sens horizontal et vertical., ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Toujours est-il que le filtrage de texture est une opération assez lourde, qui demande beaucoup de calculs arithmétiques. On pourrait en théorie le faire dans les pixels shaders, mais le cout en performance serait absolument insoutenable. Aussi, les cartes graphiques intègrent toutes un circuit dédié au filtrage de texture, le ''texture sampler''. Même les plus anciennes cartes graphiques incorporent une unité de filtrage de texture, ce qui nous montre à quel point cette opération est importante.
[[File:Texture unit.png|centre|vignette|upright=2.0|Unité de texture.]]
On peut configurer la carte graphique de manière à ce qu'elle fasse soit du filtrage bilinéaire, soit du filtrage trilinéaire, on peut configurer le niveau de filtrage anisotropique, etc. Cela peut se faire dans les options de la carte graphique, mais cela peut aussi être géré par l'application. La majorité des jeux vidéos permettent de régler cela dans les options. Ces réglages ne concernent pas la texture elle-même, mais plutôt la manière dont l'unité de texture doit fonctionner. Ces réglages sur l''''état de l'unité de texture''' sont mémorisés quelque part, soit dans l'unité de texture elle-même, soit fournies avec la ressource de texture elle-même, tout dépend de la carte graphique. Certaines cartes graphiques mémorisent ces réglages dans les unités de texture ou dans le processeur de commande, et tout changement demande alors de réinitialiser l'état des unités de texture, ce qui prend un peu de temps. D'autres placent ces réglages dans les ressources de texture elles-mêmes, ce qui rend les modifications de configuration plus rapides, mais demande plus de circuits. D'autres cartes graphiques mélangent les deux options, certains réglages étant globaux, d'autres transmis avec la texture. Bref, difficile de faire des généralités, tout dépend du matériel et le pilote de la carte graphique cache tout cela sous le tapis.
Maintenant que cela est dit, voyons quelles sont les différentes méthodes de filtrage de texture et comment la carte graphique fait pour les calculer.
===Le filtrage au plus proche===
La méthode de filtrage la plus simple consiste à colorier avec le texel le plus proche. Cela revient tout simplement à ne pas tenir compte de la partie fractionnaire des coordonnées x,y, ce qui est très simple à implémenter en matériel. C'est ce que l'on appelle le '''filtrage au plus proche''', aussi appelé ''nearest filtering''.
Autant être franc, le résultat est assez pixelisé et peu agréable à l’œil. Par contre, le résultat est très rapide à calculer, vu qu'il ne demande aucun calcul à proprement parler. Elle ne fait pas appel à la parti fractionnaire des coordonnées entières de texture, ni aux dérivées de ces coordonnées. On peut combiner cette technique avec le mip-mapping, ce qui donne un résultat bien meilleur, bien que loin d'être satisfaisant. Au passage, toutes les techniques de filtrage de texture peuvent se combiner avec du mip-mapping, certaines ne pouvant pas faire sans.
[[File:Interpolation-nearest.svg|centre|vignette|Filtrage de texture au plus proche.]]
===Le filtrage linéaire===
Le filtrage le plus simple est le '''filtrage linéaire'''. Il effectue une interpolation linéaire entre deux mip-maps, deux niveaux de détails. Pour comprendre l'idée, nous allons prendre une situation très simple, avec une texture carrée de 512 texels de côté. Le mip-mapping crée plusieurs textures : une de 256 texels de côté, une de 128 texels, une de 64, etc. Maintenant, la texture est sur un objet à une certaine distance de l'écran, vu de face. Le résultat est qu'elle correspond à l'écran à un carré de 300 pixels de côté (pas d'erreur : pixels, pas texels). Dans ce cas, la texture se trouve entre deux mip-maps : celle de 512 pixels de côté, celle de 256. Laquelle choisir ? Le filtrage au plus proche prend la texture de 512 pixels de côté. Le filtrage linéaire lui, fait autrement.
Vu que la texture est entre deux mip-maps, l'idée est de prendre le texel au plus proche dans chaque texture et de faire une sorte de moyenne appelée l'interpolation linéaire. L'interpolation par du principe que la couleur varie entre les deux texels en suivant une fonction affine, illustrée ci-dessous. Ce ne serait évidemment pas le cas dans le monde réel, mais on supposer cela donne une bonne approximation de ce à quoi ressemblerait une texture à plus haute résolution. On peut alors calculer la couleur du pixel par une simple moyenne pondérée par la distance. Le résultat est que les transitions entre deux niveaux de détails sont plus lisses, moins abruptes.
[[File:Lin interp -é.png|centre|vignette|upright=2.0|Interpolation linéaire.]]
===Le filtrage bilinéaire===
Le filtrage bilinéaire effectue une sorte de moyenne pondérée des quatre texels les plus proches du pixel à afficher. Pour cela, rappelez-vous ce qui a été dit plus haut : les coordonnées x,y d'un pixel ont une partie entière et une partie fractionnaire. Le filtrage au plus proche élimine les parties fractionnaires, ce qui donne une coordonnée x,y. Avec le filtrage bilinéaire, on prend les texels de coordonnées (x,y) ; (x+1,y) ; (x,y+1) ; (x+1,y+1), le pixel étant entre ces 4 texels.
Mais le filtrage ne fait pas qu'une simple moyenne, il prend en compte les parties fractionnaires pour faire la moyenne. En effet, le pixel n'est pas au milieu du carré de texel, il est quelque part mais est souvent plus proche d'un texel que des autres. Et il faut donc pondérer la moyenne par les distances aux 4 texels. Pour cela, la moyenne est calculée à partir d'interpolations linéaires. Avec 4 pixels, nous allons devoir calculer la couleur de deux points intermédiaires. La couleur de ces deux points se calcule par interpolation linéaire, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.
[[File:Bilin3.png|centre|vignette|upright=2|Filtrage bilinéaire de texture.]]
Le circuit qui permet de faire l'interpolation bilinéaire est particulièrement simple. On trouve un circuit de chaque pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu, et un pour la transparence. Chacun de ces circuit est composé de sous-circuits chargés d'effectuer une interpolation linéaire, reliés comme suit.
[[File:Texture sampler unit.png|centre|vignette|Unité de filtrage bilinéaire.]]
Vous noterez que le filtrage bilinéaire accède à 4 pixels en même temps. Fort heureusement, les textures sont stockées de manière à ce qu'on puisse charger les 4 pixels en une fois, comme on l'a vu plus haut. Le filtrage bilinéaire a de fortes chances que les 4 pixels filtrés soient dans la même ''tile'', la seule exception étant quand ils sont tout juste sur le bord d'une ''tile''.
: La console de jeu Nintendo 64 n'utilise que trois pixels au lieu de quatre dans son interpolation bilinéaire, qui en devient une interpolation quasi-bilinéaire. La raison derrière ce choix est une question de performances, comme beaucoup de décisions de ce genre. Le résultat est un rendu imparfait de certaines textures.
===Le filtrage trilinéaire===
Avec le filtrage bilinéaire, des discontinuités apparaissent sur certaines surfaces. Par exemple, pensez à une texture de sol : elle est appliquée plusieurs fois sur toute la surface du sol. A une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512*512 à 256*256, ce qui est visible pour un joueur attentif. De telles transitions sont lissées grâce au filtrage linéaire, il n'y a plus qu'à le combiner avec le filtrage bilinéaire. Rien d’incompatible : le premier filtre l'intérieur d'une mip-map, le second combine deux mip-maps.
Le filtrage trilinéaire prend les deux mip-maps les plus proches, fait un filtrage bilinéaire avec chacune, puis fait une « une moyenne » pondérée entre les deux résultats. Le circuit de filtrage trilinéaire existe en plusieurs versions. La plus simple, illustrée ci-dessous, effectue deux filtrages bilinéaires en parallèle, dans deux circuits séparés, puis combine leurs résultats avec un circuit d'interpolation linéaire. Mais ce circuit nécessite de charger 8 texels simultanément. Qui plus est, ces 8 texels ne sont pas consécutifs en mémoire, car ils sont dans deux niveaux de détails/mip-maps différents.
[[File:Parallel trilinear filtering.png|centre|vignette|upright=2.0|Unité de filtrage trilinéaire parallèle.]]
Vu qu'on lit des texels dans deux mip-maps, les texels sont lus en deux fois : 4 texels provenant de la première mip-map, suivis par les 4 texels de l'autre mip-map. Les 4 premiers texels doivent donc être mis en attente dans des registres, en attendant que les 4 autres arrivent. Une amélioration du circuit précédent gère cela en ajoutant des registres. Il lit les 4 premiers texels, les filtre avec une interpolation bilinéaire, et mémorise le résultat dans un registre. Puis, il lit les 4 autres texels, les filtre, et met le résultat dans un second registre. A ce moment là, un circuit d'interpolation linéaire finit le travail. On économise donc un circuit d'interpolation bilinéaire, sans que les performances soient trop impactées.
[[File:Filtrage trilineaire.png|centre|vignette|upright=1.0|Unité de filtrage trilineaire série.]]
Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le temps où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. À chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients, et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.
===Le filtrage anisotrope===
D'autres artefacts peuvent survenir lors de l'application d'une texture, la perspective pouvant déformer les textures et entraîner l'apparition de flou. La raison à cela est que les techniques de filtrage de texture précédentes partent du principe que la texture est vue de face. Prenez une texture carrée, par exemple. Vue de face, elle ressemble à un carré sur l'écran. Mais tournez la caméra, de manière à voir la texture de biais, avec un angle, et vous verrez que la forme de la texture sur l'écran est un trapèze, pas un carré. Cette déformation liée à la perspective n'est pas prise en compte par les méthodes de filtrage de texture précédentes. Pour le dire autrement, les techniques de filtrage précédentes partent du principe que les 4 texels qui entourent un pixel forment un carré, ce qui est vrai si la texture est vue de face, sans angle, mais ne l'est pas si la texture n'est pas perpendiculaire à l'axe de la caméra. Du point de vue de la caméra, les 4 texels forment un trapèze d'autant moins proche d'un carré que l'angle est grand.
Pour corriger cela, les chercheurs ont inventé le '''filtrage anisotrope'''. En fait, je devrais plutôt dire : LES filtrages anisotropes. Il en existe un grand nombre, dont certains ne sont pas utilisés dans les cartes graphiques actuelles, soit car ils trop gourmand en accès mémoires et en calculs pour être efficaces, soit car ils ne sont pas pratiques à mettre en œuvre. Il est très difficile de savoir quelles sont les techniques de filtrage de texture utilisées par les cartes graphiques, qu'elles soient récentes ou anciennes. Beaucoup de ces technologies sont brevetées ou gardées secrètes, et il faudrait vraiment creuser les brevets déposés par les fabricants de GPU pour en savoir plus. Les algorithmes en question seraient de plus difficiles à comprendre, les méthodes mathématiques cachées derrière ces méthodes de filtrage n'étant pas des plus simple.
[[File:Anisotropic filtering en.png|centre|vignette|upright=2|Exemple de filtrage anisotrope.]]
==La compression de textures==
Les textures les plus grosses peuvent aller jusqu'au mébioctet, ce qui est beaucoup. Pour limiter la casse, les textures sont compressées. La '''compression de texture''' réduit la taille des textures, ce qui peut se faire avec ou sans perte de qualité. Elle entraîne souvent une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande. Mais il s'agit là d'une technique très simple, beaucoup plus simple que les techniques que nous allons voir dans cette section. Nous allons voir quelque algorithmes de compression de textures de complexité intermédiaire, mais n'allons pas voir l'état de l'art. Il existe des formats de texture plus récents que ceux qui nous allons aborder, comme l{{'}}''Ericsson Texture Compression'' ou l{{'}}''Adaptive Scalable Texture Compression'', plus complexes et plus efficaces.
Notons que les textures sont compressées dans les fichiers du jeu, mais aussi en mémoire vidéo. Les textures sont décompressées lors de la lecture. Pour cela, la carte graphique contient alors un circuit, capable de décompresser les textures lorsqu'on les lit en mémoire vidéo. Les cartes graphiques supportent un grand nombre de formats de textures, au niveau du circuit de décompression. Du fait que les textures sont décompressées à la volée, les techniques de compression utilisées sont assez particulières. La carte graphique ne peut pas décompresser une texture entière avant de pouvoir l'utiliser dans un ''pixel shader''. A la place, on doit pouvoir lire un morceau de texture, et le décompresser à la volée. On ne peut utiliser les méthodes de compression du JPEG, ou d'autres formats de compression d'image. Ces dernières ne permettent pas de décompresser une image morceau par morceau.
Pour permettre une décompression/compression à la volée, les textures sont des textures tilées, généralement découpées en tiles de 4 * 4 texels. Les ''tiles'' sont compressées indépendamment les unes des autres. Et surtout, avec ou sans compression, la position des tiles en mémoire ne change pas. On trouve toujours une tile tous les T octets, peu importe que la tile soit compressée ou non. Par contre, une tile compressée n'occupera pas T octets, mais moins, là où une tile compressée occupera la totalité des T octets. En clair, compresser une tile fait qu'il y a des vides entre deux tiles dans al mémoire vidéo, mais ne change rien à leur place en mémoire vidéo qui est prédéterminée, peu importe que la texture soit compressée ou non. L'intérêt de la compression de textures n'est pas de réduire la taille de la texture en mémoire vidéo, mais de réduire la quantité de données à lire/écrire en mémoire vidéo. Au lieu de lire T octets pour une tile non-compressée, on pourra en lire moins.
===La palette indicée et la technique de ''Vector quantization''===
La technique de compression des textures la plus simple est celle de la '''palette indicée''', que l'on a entraperçue dans le chapitre sur les cartes d'affichage. La technique de '''''vector quantization''''' peut être vue comme une amélioration de la palette, qui travaille non pas sur des texels, mais sur des ''tiles''. À l'intérieur de la carte graphique, on trouve une table qui stocke toutes les ''tiles'' possibles. Chaque ''tile'' se voit attribuer un numéro, et la texture sera composé d'une suite de ces numéros. Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l’embarqué utilisent ce genre de compression.
===Les algorithmes de ''Block Truncation coding''===
La première technique de compression élaborée est celle du '''''Block Truncation Coding''''', qui ne marche que pour les images en niveaux de gris. Le BTC ne mémorise que deux niveaux de gris par ''tile'', que nous appellerons couleur 1 et couleur 2, les deux niveaux de gris n'étant pas le même d'une ''tile'' à l'autre. Chaque pixel d'une ''tile'' est obligatoirement colorié avec un de ces niveaux de gris. Pour chaque pixel d'une ''tile'', on mémorise sa couleur avec un bit : 0 pour couleur 1, et 1 pour couleur 2. Chaque ''tile'' est donc codée par deux entiers, qui codent chacun un niveau de gris, et une suite de bits pour les pixels proprement dit. Le circuit de décompression est alors vraiment très simple, comme illustré ci-dessous.
[[File:Block Truncation coding.jpg|centre|vignette|upright=2.0|Block Truncation coding.]]
La technique du BTC peut être appliquée non pas du des niveaux de gris, mais pour chaque composante Rouge, Vert et Bleu. Dans ces conditions, chaque ''tile'' est séparée en trois sous-''tiles'' : un sous-bloc pour la composante verte, un autre pour le rouge, et un dernier pour le bleu. Cela prend donc trois fois plus de place en mémoire que le BTC pur, mais cela permet de gérer les images couleur.
===Le format de compression S3TC / DXTC===
L'algorithme de '''Color Cell Compression''', ou CCC, améliore le BTC pour qu'il gère des couleurs autre que des niveaux de gris. Ce CCC remplace les deux niveaux de gris par deux couleurs. Une ''tile'' est donc codée avec un entier 32 bits par couleur, et une suite de bits pour les pixels. Le circuit de décompression est identique à celui utilisé pour le BTC.
[[File:Color Cell Compression.jpg|centre|vignette|Color Cell Compression.]]
[[File:Dxt1-memory-layout.png|vignette|Dxt1 et ''color cell compression''.]]
Le format de compression de texture utilisé de base par Direct X, le DXTC, est une version amliorée de l'algorithme précédent. Il est décliné en plusieurs versions : DXTC1, DXTC2, etc. La première version du DXTC est une sorte d'amélioration du CCC : il ajoute une gestion minimale de transparence, et découpe la texture à compresser en ''tiles'' de 4 pixels de côté. La différence, c'est que la couleur finale d'un texel est un mélange des deux couleurs attribuée au bloc. Pour indiquer comment faire ce mélange, on trouve deux bits de contrôle par texel.
Si jamais la couleur 1 < couleur2, ces deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (2 * Couleur1 + Couleur2) / 3
* 11 = (Couleur1 + 2 * Couleur2) / 3
Sinon, les deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (Couleur1 + Couleur2) / 2
* 11 = Transparent
[[File:DXTC.jpg|centre|vignette|DXTC.]]
Le circuit de décompression du DXTC ressemble alors à ceci :
[[File:Circuit de décompression du DXTC.jpg|centre|vignette|upright=2.0|Circuit de décompression du DXTC.]]
===Les format DXTC 2, 3, 4 et 5 : l'ajout de la transparence===
Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3, lui-même replacé par le DXT4 et par le DXT5. Dans le DXT3, la transparence fait son apparition. Pour cela, on ajoute 64 bits par ''tile'' pour stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1.
[[File:Dxt23-memory-layout.png|centre|vignette|Dxt 2 et 3.]]
Dans le DXT4 et le DXT5, la méthode utilisée pour compresser les couleurs l'est aussi pour les valeurs de transparence. L'information de transparence est stockée par un en-tête contenant deux valeurs de transparence, le tout suivi d'une matrice qui attribue trois bits à chaque texel. En fonction de la valeur des trois bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1.
[[File:Dxt45-memory-layout.png|centre|vignette|Dxt 4 et 5.]]
===Le format de compression PVRTC===
Passons maintenant à un format de compression de texture un peu moins connu, mais pourtant omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle travaille surtout dans les cartes graphiques embarquées. Ses cartes se trouvent notamment dans l'ipad, l'iPhone, et bien d'autres smartphones actuels.
Avec le PVRTC, les textures sont encore une fois découpées en ''tiles'' de 4 texels par 4, mais la ressemblance avec le DXTC s’arrête là. Chacque ''tile'' est codée avec :
* une couleur codée sur 16 bits ;
* une couleur codée sur 15 bits ;
* 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
* et un bit de modulation, qui permet de configurer l’interprétation des bits de mélange.
Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4*4.
==Annexe : le ''normal-mapping'' hardware==
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, parlons un peu du ''normal mapping'' et de son implémentation dans les unités de texture. Pour rappel, 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. Et pour comprendre quel est le rapport avec les textures, nous allons devoir faire quelques rappels sur l'éclairage par pixel.
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
L'éclairage se fait en utilisant de nombreux calculs, qu'on a détaillé dans le chapitre sur les bases du rendu 3D. Ceux-ci utilisent la normale d'un sommet, à savoir un vecteur orienté à la verticale de la surface. En théorie, il y a une normale par sommet, ce qui fait que les calculs d'éclairage doivent se faire au niveau géométrique, avant la rastérisation. Mais pour obtenir un éclairage de meilleure qualité, il y a des techniques qui calcule l'éclairage d'une scène 3D pixel par pixel.
Une première technique d'éclairage par pixel interpole les normales lors de l'étape de rastérisation. On obtient alors un éclairage de Phong. Une autre solution est celle du ''normal mapping''. Le '''''normal mapping''''' précalcule les normales d'une surface dans une texture, appelée la ''normal map''. L'éclairage est alors réalisé par un pixel shader, qui lit les normales depuis cette texture et fait les calculs d'éclairage avec.
===Les ''normal maps'' sont des textures===
Notons que l'accès à la ''normal map'' se fait comme pour n'importe quelle texture : les texels sont lus en mémoire vidéo, puis le filtrage de texture est appliqué, et enfin le tout est envoyé au ''pixel shader''. En théorie, le filtrage effectue une sorte d'interpolation des normales automatique, ce qui permet de trouver la normale à un pixel précis, même s'il n'est pas sur un texel.
La ''normal map'' est une texture, donc. Mais ce n'est pas une texture comme une autre. Elle mémorise un vecteur pour chaque texel, pas une couleur. Il est possible d'utiliser les formats de textures vus plus haut, même si cela ne donne pas de bons résultats. Après tout, un vecteur est codé par trois coordonnées x,y,z, il est possible de coder chacune avec un octet et de packager cela dans une texture RGB classique. La coordonnée x va dans la composante Rouge, la coordonnée Y dans la composante Bleu, et la coordonnée z dans la composante z.
Le problème est que le rendu final n'est pas très beau. Et les opérations de filtrage ne donnent pas de très bons résultats. Pour remédier à ces problèmes, des formats de texture spécialisés pour les ''normal map'' ont été inventés. Le premier d'entre eux est le '''3Dc''', aussi appelé BC5 pour ''Block Compression 5''. Il utilise un octet par texel, au lieu de trois avec une texture RGB normale non-compressée.
===Le format de texture 3Dc===
Avec le 3Dc, les trois coordonnées des vecteurs ne sont pas mémorisées, seules deux d'entre elles le sont. En effet, les normales sont des vecteurs ''normalisés'', c'est à dire que leur longueur vaut 1, par construction. Vu que la taille est connue à l'avance, on peut en déduire la coordonnée z à partir des coordonnées x et y, avec l'usage du théorème de Pythagore. Par définition, <math>z^2 = 1 - x^2 - y^2</math>. Le calcul peut être fait dans le ''pixel shader'' sans problème.
Pour le reste, la compression de la ''normal map'' ressemble un peu à celle des textures. La ''normal map'' est découpée en blocs de 4 par 4 texels de côté, chacune étant encodée en tenant compte de certaines redondances. Dans un bloc, les coordonnées x sont dans un certain intervalle, allant de <math>x_{min}</math> à <math>x_{max}</math>, idem pour la coordonnée y qui est dans l'intervalle <math>y_{min}</math>, <math>y_{max}</math>. L'intervalle <math>x_{min}</math> à <math>x_{max}</math> est coupé en 8 parts égales, ce qui fait 8 valeurs possibles dans cet intervalle, chacune correspondant à une valeur x possible pour une normale du bloc. La normale est donc encodée en précisant quelle valeur est la bonne, en utilisant un indice de 3 bits pour cela. La valeur exacte de la coordonnée x se calcule à partir de l'indice comme suit :
: <math>x = x_{min} + \left( \frac{x_{min} - x_{min}}{8} \right) \times \text{indice} </math>
Pour encoder un bloc de 4 pr 4 normales, il faut donc :
* Un octet pour chaque coordonnée <math>x_{min}</math>, <math>x_{max}</math>, <math>y_{min}</math>, <math>y_{max}</math>.
* 6 bits par normale : 3 pour encoder le décalage de la coordonnée x, 3 pour la coordonnée y.
Le tout permet d'encoder un bloc de 16 normales en seulement 128 bits, soit un octet par normale. Le ''pixel shader'' se débrouille pour décompresser un bloc. Il effectue notamment le calcul du dessus, pour retrouver la coordonnée x. Les calculs étant particulièrement simples, le cout en performance est très faible. Et ce d'autant plus qu'ils demandent juste de faire des additions et des multiplications entières, que l'unité scalaire peut faire en parallèle d'autres opérations plus gourmandes..
==Annexe : les ''shadowmap'' hardware==
Les anciens GPU, notamment la Geforce FX, avaient des fonctionnalités spécifiques pour le calcul des ombres. Dans la plupart des jeux vidéos de l'époque, et même de maintenant, les ombres sont calculées avec la technique des ''shadowmap''. L'idée est assez simple sur le principe : un pixel est dans l'ombre quand il est invisible depuis une source de lumière.
L'idée est que le rendu est réalisé en plusieurs passes, avec une passe par source de lumière et une passe finale pour calculer l'image finale. Nous allons expliquer la technique avec une seule source de lumière, et allons utiliser l'exemple de la scène ci-dessous.
[[File:7fin.png|centre|vignette|Scène 3D d'exemple.]]
===La technique du ''shadowmapping''===
[[File:2shadowmap.png|vignette|Résultat de la première passe : ''shadowmap''..]]
La première passe rend l'image depuis le point de vue de la source de lumière. Cette première passe ne rend pas les couleurs de la scène, elle ne s'intéresse qu'à la profondeur des pixels. Le résultat est que l'image ne rend que le tampon de profondeur. Celui-ci est ensuite réutilisé comme texture pour la passe suivante. La texture en question est appelée la '''''shadownmap'''''.
La perspective utilisée, ainsi que le ''view frustrum'', dépend de la source de lumière. Pour une source de lumière qui émet un cône de lumière, le ''view frustrum'' de l'image rendue doit contenir tout le cone de lumière, et doit coller le plus possible à celui-ci. Pour une source directionnelle, comme le soleil, une perspective orthographique est utilisée.
La seconde passe rend l'image du point de vue de la caméra, pour rendre l'image finale. Elle rend l'image finale, qui est composée de pixels, chacun ayant une position à l'écran x,y, et une profondeur z. Les coordonnées sont transformées pour obtenir la position de ce pixel depuis le point de vue de la caméra. Une simple multiplication de matrice suffit, rien de bien compliqué, un shader peut le faire.
[[File:5failed.png|vignette|Résultat du test des comparaisons.]]
Après cette étape, on a alors les coordonnées x,y,z de ce pixel du point de vue de la caméra, et la ''shadowmap''. Il est alors possible d'accéder à la ''shadowmap'' au même endroit, à la même place que le pixel testé, aux mêmes coordonnées x,y. Si la profondeur du pixel est supérieure à celle de la shadowmap au même endroit, alors le pixel est situé derrière la surface visible, donc est dans l'ombre. Sinon, il n'est pas dans l'ombre. Le même procédé est répété sur chaque pixel de l'écran.
===Les optimisations hardware du ''shadowmapping''===
La technique des ''shadowmap'' demande donc de calculer une texture ''shadowmap'', puis de lire celle-ci et de faire des comparaisons de profondeur. Les GPU comme la Geforce FX intégraient du matériel dans les unités de texture pour faciliter ce travail. Les unités de texture pouvaient lire les ''shadowmap'', et faire la comparaison de profondeur toutes seules, elles avaient des circuits pour. Il suffisait de leur fournir le pixel à tester, ses coordonnées x,y,z, et l'adresse de la ''shadowmap''. Les unités de texture renvoyaient alors un résultat valant 0 ou 1 : 1 si le pixel est dans l'ombre, 0 sinon.
Elles pouvaient même effectuer du filtrage de texture sur les ''shadowmap''. Mais le filtrage était différent de celui utilisé sur les autres textures : moyenner des valeurs de profondeur ne marche pas bien. Elles utilisaient des techniques de filtrage différentes : elles faisaient les tests de comparaison, puis faisaient la moyenne des résultats. Ainsi, pour du filtrage bilinéaire, elles lisaient 4 texels dans la ''shadowmap'', puis faisaient 4 tests de comparaison, et moyennaient les 4 résultats.
==Annexe : les textures virtuelles==
Les '''textures virtuelles''' sont une optimisation des textures normales, qui visent à accélérer le rendu de terrains de grande taille. Imaginez par exemple un monde assez ouvert, comme un environnement en forêt ou en montagne, avec une grande distance de visibilité. Avec de tels terrains, le "sol" est recouvert par une texture de sol unique qui recouvre tout le terrain. Elle ne se répète pas, est de très grande taille, et peut parfois recouvrir toute la map ! Mais il n'y a pas assez de mémoire vidéo pour mémoriser la texture toute entière. La seule solution est la suivante : une partie de la texture est placée en mémoire vidéo, le reste est soit placé en mémoire RAM ou sur le disque dur.
Pour cela, le moteur de jeu utilise une optimisation ingénieuse, basée sur une observation assez basique : une bonne partie de la texture est visible, mais le reste est caché par des arbres, des habitations ou d'autres obstacles. Une optimisation possible de ne garder en mémoire vidéo que les portions visibles de la texture, pas les portions cachées. Une autre optimisation mélange textures virtuelles et ''mip-mapping''. L'idée est que pour les portions lointaines d'une texture, la texture utilisée est une ''mip-map'' de basse résolution. L'idée est alors de ne charger que la ''mip-map'' adéquate, pas les autres niveaux de détail. En clair, la texture de base n'est pas chargée en mémoire vidéo, mais la ''mip-map'' basse résolution l'est.
===Une texture à deux niveaux===
L'implémentation des textures virtuelles découpe les méga-textures en ''tiles'', en morceaux rectangulaires de taille modeste. En clair, le terrain est découpé en morceau rectangulaires/carrés. Seules les tiles nécessaires sont chargées en mémoire vidéo, pas les autres. Par exemple, les ''tiles'' non-visibles ne sont pas placées en mémoire vidéo, seules les ''tiles'' visibles le sont. De même, il y a une ''tile'' par niveau de mip-map : seul la tile correspondant le niveau adéquat est en mémoire vidéo, les autres niveaux de détail ne sont pas chargés. On peut faire une analogie avec la mémoire virtuelle, où les données sont découpées en pages, qui sont chargées en mémoire RAM à la demande, suivant les besoins, les données pouvant être swappées sur le disque dur si elles sont peu utilisées. Sauf qu'ici, il s'agit de textures qui sont découpées en pages chargées à la demande en mémoire vidéo, depuis la RAM système.
Une texture virtuelle est en réalité un système à deux niveaux : une liste de ''tiles'' et les ''tiles'' elles-mêmes. La liste de ''tiles'' est appelée un '''atlas de texture''', c'est un peu l'équivalent de la ''tilemap'' pour le rendu 2D. Rendre une texture demande de calculer quelle ''tile'' contient le texel à afficher, consulter la ''tile'' en question, puis récupérer le texel adéquat dans cette ''tile''. La ''tile'' est donc une texture, mais la texture à charger est choisie parmi un ensemble, qui est ici l'atlas de texture.
===L'implémentation : logicielle versus matérielle===
Les textures virtuelles ont été utilisées pour la première fois par les jeux Rage 1 et 2 d'IdSoftware, et quelques jeux ultérieurs comme DOOM 2016. IdSoftware les appelait des '''''mega-textures'''''. L'optimisation permettait des gains en performance assez impressionnants. Le jeu Rage 1 utilisait une texture carrée unique de 128k pixels de côté pour rendre le terrain. En théorie, une telle texture devrait prendre 64 giga-octets, mais le jeu tournait correctement avec 512 méga-octets de RAM, poussivement avec seulement 256 méga-octets de RAM.
De nos jours, les textures virtuelles sont supportées par beaucoup de jeux vidéos, les moteurs les plus courants gèrent de telles textures de manière logicielles. Mais quelques GPU récents supportent les textures virtuelles. Sur les GPU récents, l'atlas de texture est géré nativement par le matériel. Le GPU choisit quelle ''tile'', quelle texture choisir pour rendre le texel adéquat. Pour cela, le GPU calcule quelle ''tile'' charger, consulte l'atlas de texture, et lit la texture de ''tile'' adéquate.
Mais l'implémentation sur les GPU récents a de nombreuses limitations. La limitation la plus importante est que la taille des textures virtuelles ne peut pas dépasser la taille d'une texture normale, soit 32768 pixels de côté pour une texture carrée environ sur les GPU de 2020. De plus, le chargement d'une ''tile'' est très lent. En clair, dès qu'on veut changer de niveau de mip-map pour une tile, ou dès qu'une tile devient visible, le chargement de la tile peut facilement prendre plusieurs centaines de millisecondes. Le filtrage de texture est très complexe avec des textures virtuelles, ce qui fait que le filtrage de texture virtuelle est souvent soumis à des limitations que les textures normales n'ont pas, notamment pour le filtrage anisotropique.
{{NavChapitre | book=Les cartes graphiques
| prev=Le rasterizeur
| prevText=Le rasterizeur
| next=Les Render Output Target
| nextText=Les Render Output Target
}}{{autocat}}
hfwinxzspa2az136egejzvg4cutfr6p
763411
763410
2026-04-10T17:11:27Z
Mewtow
31375
/* Le format de texture 3Dc */
763411
wikitext
text/x-wiki
[[File:Texture mapping.png|vignette|''Texture mapping'']]
Les '''textures''' sont des images que l'on va plaquer sur la surface d'un objet, du papier peint en quelque sorte. Les cartes graphiques supportent divers formats de textures, qui indiquent comment les pixels de l'image sont stockés en mémoire : RGB, RGBA, niveaux de gris, etc. Une texture est donc composée de "pixels", comme toute image numérique. Pour bien faire la différence entre les pixels d'une texture, et les pixels de l'écran, les pixels d'une texture sont couramment appelés des ''texels''.
==Le placage de textures inverse==
Pour rappel, plaquer une texture sur un objet consiste à attribuer un texel à chaque sommet, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture.
Dans les faits, on n'utilise pas de coordonnées entières de ce type. Les coordonnées de texture sont deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. Le nom donnée à cette technique de description des coordonnées de texture s'appelle l''''''UV Mapping'''''.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Les API 3D modernes gèrent des textures en trois dimensions, ce qui ajoute une troisième coordonnée de texture notée w. Dans ce qui va suivre, nous allons passer les textures en trois dimensions sous silence. Elles ne sont pas très utilisées, la quasi-totalité des jeux vidéo et applications 3D utilisant des textures en deux dimensions. Par contre, le matériel doit gérer les textures 3D, ce qui le rend plus complexe que prévu. Il faut ajouter quelques circuits pour, de quoi gérer la troisième coordonnée de texture, etc.
Lors de la rastérisation, chaque fragment se voit attribuer un sommet, et donc la coordonnée de texture qui va avec. Si un pixel est situé pile sur un sommet, la coordonnée de texture de ce sommet est attribuée au pixel. Si ce n'est pas le cas, la coordonnée de texture finale est interpolée à partir des coordonnées des trois sommets du triangle rastérisé. L'interpolation en question a lieu dans l'étape de rastérisation, comme nous l'avons vu dans le chapitre précédent. Le fait qu'il y ait une interpolation fait que les coordonnées du pixel gagent à être des nombres flottants. On pourrait faire une interpolation avec des coordonnées de texture entières, mais les arrondis et autres imprécisions de calcul donneraient un résultat graphiquement pas terrible, et empêcheraient d'utiliser les techniques de filtrage de texture que nous verrons dans ce chapitre.
À partir de ces coordonnées de texture, la carte graphique calcule l'adresse du texel qui correspond, et se charge de le lire. Et toute la magie a lieu dans ce calcul d'adresse, qui part de coordonnées de texture flottante, pour arriver à une adresse mémoire. Le calcul de l'adresse du texel se fait en plusieurs étapes, que nous allons voir ci-dessous. La première étape convertit les coordonnées flottantes en coordonnées entières, qui disent à quel ligne et colonne se trouve le texel voulu dans la texture. L'étape suivante transforme ces coordonnées x,y entières en adresse mémoire.
===La normalisation des coordonnées===
J'ai dit plus haut que les coordonnées de texture sont des coordonnées flottantes, comprises entre 0 et 1. Mais il faut savoir que les pixels shaders peuvent modifier celles-ci pour mettre en œuvre certains effets graphiques. Et le résultat peut alors se retrouver en-dehors de l'intervalle 0,1. C'est quelque chose de voulu et qui est traité par la carte graphique automatiquement, sans que ce soit une erreur. Au contraire, la manière dont la carte graphique traite cette situation permet d'implémenter des effets graphiques comme des textures en damier ou en miroir.
[[File:Clamp tile.jpg|vignette|Clamp tile]]
Il existe globalement trois méthodes très simples pour gérer cette situation, qui sont appelés des '''modes d'adressage de texture'''.
* La première méthode est de faire en sorte que le résultat sature. Si une coordonnée est inférieur à 0, alors on la remplace par un zéro. Si elle est supérieure à 1, on la ramène à 1. Avec cette méthode, tout se passe comme si les bords de la texture étaient étendus et remplissaient tout l'espace autour de la texture. Le tout est illustré ci-dessous. Ce mode d'accès aux textures est appelé le '''''clamp'''''.
* Une autre solution retire la partie entière de la coordonnée, elle coupe tout ce qui dépasse 1. Pour le dire autrement, elle calcule le résultat modulo 1 de la coordonnée. Le résultat est que tout se passe comme si la texture était répétée à l'infini et qu'elle pavait le plan.
* Une autre méthode remplit les coordonnées qui sortent de l’intervalle 0,1 avec une couleur préétablie, configurée par le programmeur.
===La conversion des coordonnées de textures flottantes en adresse mémoire===
Une fois la normalisation effectuée, les coordonnées de texture sont utilisées pour lire le texel voulu. Pour cela, les coordonnées de texte sont transformées en adresse mémoire, adresse qui pointe sur le texel ayant ces cordonnées. Pour cela, la première étape est de transformer les coordonnées flottantes u,v en coordonnées entières x,y qui pointent sur un texel. Pour cela, il suffit de multiplier les coordonnées flottantes u,v par la résolution de la texture accédée. Pour un écran de résolution <math>\text{height,width}</math>, le calcul est le suivant :
: <math>x = u \times \text{width}</math>
: <math>y = v \times \text{height}</math>
Le résultat est un nombre avec une partie entière et une partie fractionnaire. La partie entière des deux coordonnées donne la position x,y voulue, et la partie fractionnaire est conservée pour le filtrage de textures, mais passons cela sous silence pour le moment.
La seconde étape prend les coordonnées entières x,y et calcule l'adresse mémoire du texel. L'adresse dépend de la position de la texture en mémoire, précisément de son début, son premier texel, mais aussi de la position du texel par rapport au début de la texture. Et calculer cette position intra-texture dépend de la manière dont les texels sont stockés en mémoire.
====Les textures naïves====
Les programmeurs qui lisent ce cours s'attendent certainement à ce que la texture soit stockée en mémoire ligne par ligne, ou colonne par colonne. Cela veut dire que le premier pixel en partant d'en haut à gauche est stocké en premier, puis celui immédiatement à sa droite, puis celui encore à droite, et ainsi de suite. Une fois qu'on arrive à la fin d'une ligne, on passe à la ligne suivante, en-dessous. Cette organisation ligne par ligne s'appele l'organisation '''''row major order'''''. On peut faire pareil, mais colonne par colonne, ce qui donne le '''''column major order'''''.
[[File:Speicheranordnung Feld.svg|centre|vignette|upright=2|Row et column major order.]]
Maintenant, supposons que la texture commence à l'adresse <math>A_\text{texture}</math>, qui est l'adresse du premier texel. La texture a une résolution de <math>\text{width}</math> texels de large et <math>\text{height}</math> texels de haut. Par définition, les coordonnées X et Y des texels commencent à 0, ce qui fait que le pixel en haut à gauche a les coordonnées 0,0.
L'adresse du pixel se calcule comme suit :
: <math>A_\text{pixel} = A_\text{texture} + (\text{taille d'une ligne en octets} \times Y) + (\text{taille d'un texel en octets} \times X)</math>
La taille d'un pixel en mémoire est notée T. La taille d'une ligne en mémoire est de <math>width \times T</math>, par définition, vu qu'elle fait <math>width</math> texels. On a donc :
: <math>A_\text{pixel} = A_\text{texture} + (width \times T \times Y) + (T \times X)</math>
La formule se réécrit comme suit :
: <math>A_\text{pixel} = A_\text{texture} + T \times (width \times Y + X)</math>
Le calcul d'adresse est donc assez simple. Malheureusement, les textures ne sont pas stockées de cette manière en mémoire vidéo. En effet, elle se marie mal avec les opérations de filtrage de texture que nous allons voir dans ce qui suit. Le filtrage d'un texel dépend de ses voisins du dessus et du dessous. Le fait que la texture n'est pas forcément parcourue ligne par ligne fait que stocker une texture ligne par ligne n'est pas l'idéal.
De même, les textures sont déformées par la perspective. L'affichage de la texture ne se fait alors pas ligne par ligne, mais en parcourant la texture en diagonale, l'angle de la diagonale correspondant approximativement à l'angle que fait la verticale de la texture avec le regard. Vu qu'on ne connait pas à l'avance l'angle que fera la diagonale de parcours, on doit ruser.
====Les textures tilées====
Une première solution à ce problème est celle des '''textures tilées'''. Avec ces textures, l'image de la texture est découpée en ''tiles'', des rectangles ou en carrés de taille fixe, généralement des carrés de 4 pixels de côté. Les tiles ont une largeur et une longueur égales, afin de simplifier les calculs : on divise X et Y par le même nombre. De plus, leur largeur et leur longueur sont une puissance de deux, afin de simplifier les calculs d'adresse. Les ''tiles'' sont alors mémorisée les unes après les autres dans le fichier de la texture.
[[File:Texture tilée.png|centre|vignette|upright=2|Texture tilée]]
La formule de calcul d'adresse vue plus haut doit être adaptée pour tenir compte des tiles. Pour cela, il faut remplacer la taille d'un texel par la taille d'une tile, et que la largeur de la texture soit exprimée en nombre de tiles. De plus, on doit adapter les coordonnées des texels pour donner des coordonnées de tile. Généralement, les tiles sont des carrés de N pixels de côté, ce qui fait qu'on peut regrouper les lignes et les colonnes par paquets de N. Il suffit donc de diviser Y et X pour obtenir les coordonnées de la tile, de même que la larguer. La formule pour calculer la position de la énième tile est alors la suivante :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + \text{Taille mémoire d'une tile} \times \left( {\text{Width} \over N} \times {Y \over N} + {X \over N} \right)</math>
On peut réécrire le tout comme suit :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + K \times \left( {Y \over N} + X \right)</math>, avec K une constante connue à la compilation des shaders.
Vu que les tiles sont carrées avec une largeur qui est une puissance de deux, la multiplication par la taille d'une tile en mémoire se simplifie : on passe d'une multiplication entière à des décalages de bits. Même chose pour le calcul de l'adresse de la tile à partir des coordonnées x,y : ils impliquent des divisions par une puissance de deux, qui deviennent de simples décalages.
La position d'un pixel dans une tile dépend du format de la texture, mais peut se calculer avec quelques calculs arithmétiques simples. Dans les cas les plus simples, les pixels sont mémorisés ligne par ligne, ou colonne par colonne. Mais ce n'est pas systématiquement le cas. Toujours est-il que les calculs pour déterminer l'adresse sont simples, et ne demandent que quelques additions ou multiplications. Mais avec les formats de texture utilisés actuellement, les tiles sont chargées en entier dans le cache de texture, sans compter que diverses techniques de compression viennent mettre le bazar, comme on le verra dans la suite de cours.
Un avantage de l'organisation en tiles est qu'elle se marie bien avec le parcours des textures. On peut parcourir une texture dans tous les sens, horizontal, vertical, ou diagonal, on sait que les prochains pixels ont de fortes chances d'être dans la même tile. Si on rentre dans une tile par la gauche en haut, on a encore quelques pixels à parcourir dans la tile, par exemple. De même, le filtrage de textures est facilité. On verra dans ce qui va suivre que le filtrage de texture a besoin de lire des blocs de 4 texels, des carrés de 2 pixels de côté. Avec l'organisation en tile, on est certain que les 4 texels seront dans la même tile, sauf s'ils ont le malheur d'être tout au bord d'une tile. Ce dernier cas est assez rare, et il l'est d'autant plus que les tiles sont grandes. Enfin, un dernier avantage est que les tiles sont généralement assez petites pour tenir tout entier dans une ligne de cache. Le cache de texture est donc utilisé à merveille, ce qui rend les accès aux textures plus rapides.
====Les textures basées sur des ''z-order curves''====
Les formats de textures théoriquement optimaux utilisent une '''''Z-order curve''''', illustrée ci-dessous. L'idée est de découper la texture en quatre rectangles identiques, et de stocker ceux-ci les uns à la suite des autres. L'intérieur de ces rectangles est lui aussi découpé en quatre rectangles, et ainsi de suite. Au final, l'ordre des pixels en mémoire est celui illustré ci-dessous.
[[File:Z-CURVE.svg|centre|vignette|upright=2|Construction d'une ''Z-order curve''.]]
Les texels sont stockés les uns à la suite des autres dans la mémoire, en suivant l'ordre donnée par la ''Z-order curve''. Le calcul d'adresse calcule la position du texel en mémoire, par rapport au début de la texture, et ajoute l'adresse du début de la texture. Mais tout le défi est de calculer la position d'un texel en mémoire, à partir des coordonnées x,y. Le calcul peut sembler très compliqué, mais il n'en est rien. Le calcul demande juste de regarder les bits des deux coordonnées et de les combiner d'une manière particulièrement simple. Il suffit de placer le bit de poids fort de la coordonnée x, suivi de celui de la coordonnée y, et de faire ainsi de suite en passant aux bits suivants.
[[File:Zcurve45bits.png|centre|vignette|upright=1.5|Calcul de la position d'un élément dans une ''Z-order curve'' à partir des coordonnées x et y.]]
L'avantage d'une telle organisation est que la textures est découpées en ''tiles'' rectangulaires d'une certaine taille, elles-mêmes découpées en ''tiles'' plus petites, etc. Et il se trouve que cette organisation est parfaite pour le cache de texture. L'idéal pour le cache de texture est de charger une ''tile'' complète dans le cache de textures. Quand on accède à un texel, on s'assure que la ''tile'' complète soit chargée. Mais cela demande de connaitre à l'avance la taille d'une ''tile''. Les formats de texture fournissent généralement une ''tile'' carré de 4 pixels de côté, mais cela donnerait un cache trop petit pour être vraiment utile. Avec cette méthode, on s'assure qu'il y ait une ''tile'' avec la taille optimale. Les ''tiles'' étant découpées en ''tiles'' plus petites, elles-mêmes découpées, et ainsi de suite, on s'assure que la texture est découpées en ''tiles'' de taille variées. Il y aura au moins une ''tile'' qui rentrera tout pile dans le cache.
==Les techniques de rendu à textures multiples==
Nous venons de voir comment une texture est plaquée sur un objet 3D, ou une surface comme un sol. Pour résumer, le calcul de l'adresse d'un texel prend la position du texel par rapport au début de la texture, et ajoute l'adresse du début de la texture. L'adresse mémoire de la texture est connue au moment où le pilote de la carte graphique place la texture dans la mémoire vidéo, et cette information est transmise au matériel par l'intermédiaire du processeur de commande, puis passée aux processeurs de shaders et à l'unité de texture. Le tout est couplé à d'autres informations, la plus importante étant la ''taille de la texture en octets'', pour éviter de déborder lors des accès à la texture.
Néanmoins, il s'agit là du cas le plus simple. Certaines techniques de rendu demandent de choisir la texture à plaquer parmi un ensemble de plusieurs textures. Les techniques en question sont assez variées et n'ont pas grand chose en commun. Les plus connues sont le ''mip-mapping'', le ''cube-mapping'' et les textures virtuelles. Le ''mip-mapping'' sert à filtrer les textures, chose qu'on expliquera plus tard, le ''cube-mapping'' sert à simuler des réflexions sur un objet en plaquant une texture de l'environnement dessus, les textures virtuelles sont une optimisation pour les textures des terrains de grande taille. Mais malgré leurs différences, elles demandent de choisir quelle texture plaquer entre plusieurs textures de base. En clair, l'adresse de base de la texture varie selon la situation. Voyons-les dans le détail.
===Le mip-mapping===
Le '''mip-mapping''' a pour but de légèrement améliorer les graphismes des objets lointains, tout en rendant les calculs de texture plus rapides. Formellement, le ''mip-mapping'' est une technique de filtrage de texture, mais nous l'abordons maintenant car elle est surtout liée au calcul d'adresse. Les unités de texture ont des circuits de filtrage de texture séparés des circuits de ''mip-mapping'' et de calcul d'adresse, d'où le fait que nous en parlons séparément.
Le problème résolu par le ''mip-mapping'' est le rendu des textures lointaines. Si une texture est plaquée sur un objet lointain, une bonne partie des détails est invisible pour l'utilisateur. Un pixel de l'écran est associé à plusieurs texels. Idéalement, la carte graphiques devrait lire tous ces texels et en faire une sorte de moyenne pondérée, pour calculer la couleur finale du pixel. Mais dans les faits, ce serait très gourmand et compliqué à implémenter en hardware. Une solution serait de ne garder que quelque texels, mais cela a tendance à créer des artefacts visuels (les textures affichées ont tendance à pixeliser). Le ''mip-mapping'' permet de réduire ces deux problèmes en même temps en précalculant cette moyenne pondérée pour des distances prédéfinies.
L'idée est d'utiliser plusieurs exemplaires d'une même texture à des résolutions différentes, chaque exemplaire étant adapté à une certaine distance. Par exemple, une texture sera stocké avec un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre de 128 * 128 et ainsi de suite jusqu’à un dernier exemplaire de 32 * 32 pixel. Chaque exemplaire correspond à un '''niveau de détail''', aussi appelé ''Level Of Detail'' (abrévié en LOD). La résolution utilisée diminue d'autant plus que l'objet est situé loin de la caméra. Les objets proches seront rendus avec la texture 512*512, ceux plus lointains seront rendus avec la texture de résolution 256*256, les textures 128*128 seront utilisées encore plus loin, et ainsi de suite jusqu'aux objets les plus lointains qui sont rendus avec la texture la plus petite de 32*32.
[[File:MipMap Example STS101.jpg|centre|vignette|upright=2|Exemples de mip-maps.]]
Le ''mip-mapping'' améliore grandement la qualité d'image. L'image d'exemple ci-dessous le montre assez bien.
[[File:Mipmapping example.png|centre|vignette|upright=2|Exemple de mipmapping.]]
Pour faciliter les calculs d'adresse, les LOD d'une même texture sont stockées les uns après les autres en mémoire (dans un tableau, comme diraient les programmeurs). Ainsi, pas besoin de se souvenir de la position en mémoire de chaque LOD : l'adresse de la texture de base, et quelques astuces arithmétiques suffisent. Prenons le cas où la texture de base a une taille L. le premier exemplaire est à l'adresse 0, le second niveau de détail est à l'adresse L, le troisième à l'adresse L + L/4, le suivant à l'adresse L + L/4 + L/16, et ainsi de suite. Le calcul d'adresse demande juste connaître le niveau de détails souhaité et l'adresse de base de la texture. Le niveau de détail voulu est calculé par les pixel shaders, en fonction de la coordonnée de profondeur du pixel à traiter.
Évidemment, cette technique consomme de la mémoire vidéo, vu que chaque texture est dupliquée en plusieurs exemplaires, en plusieurs LOD. Dans le détail, la technique du mip-mapping prend au maximum 33% de mémoire en plus (sans compression). Cela vient du fait qu'en prenant une texture dexu fois plus petite, elle prend 4 fois moins de mémoire : 2 fois moins de pixels en largeur, et 2 fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des LODs, texture de base comprise, prendra X + (X/4) + (X/16) + (X/256) + … Un petit calcul de limite donne 4/3 * X, soit 33% de plus.
===Le cube-mapping===
[[File:Cube mapped reflection example 2.JPG|vignette|Exemple de reflets environnementaux.]]
L''''environnement-mapping''' est une technique de calcul de divers effets graphiques liés à l'environnement, notamment des réflexions. L'idée est de plaquer une texture pré-calculée pour simuler l'effet de l'environnement sur une surface ou un objet 3D. Il en existe plusieurs versions différentes, mais la seule utilisée de nos jours est le ''cube-mapping'', où la texture de l'environnement est plaquée sur un cube, d'où son nom. Le cube en question est utilisé différemment suivant ce que l'on cherche à faire avec le ''cube-mapping''. Les deux utilisations principales sont le rendu du ciel et des décors, et les réflexions sur la surface des objets. Dans les deux cas, l'idée est de précalculer ce que l'on voit du point de vue de la caméra. On place la caméra dans la scène 3D, on place un cube centré sur la caméra, le cube est texturé avec ce que l'on voit de l'environnement depuis la caméra/l'objet de son point de vue.
[[File:Panorama cube map.png|centre|vignette|upright=2|L'illustration montre en premier lieu une ''cubemap'' avec les six faces mises en évidence, puis quel environnement 3D elle permet de simuler, le troisième illustration montrant comment la ''cubemap'' est utilisée pour simuler l'environnement.]]
Le rendu du ciel et des décors lointains dans les jeux vidéo se base sur des '''''skybox''''', à savoir un cube centré sur la caméra, sur lequel on ajoute des textures de ciel ou de décors lointains. Le cube est recouvert par une texture, qui correspond à ce que l'on voit quand on dirige le regard de la caméra vers cette face. Contrairement à ce qu'on pourrait croire, la skybox n'est pas les limites de la scène 3D, les limites du niveau d'un jeu vidéo ou quoique ce soit d'autre de lié à la physique de la scène 3D. La skybox est centrée sur la caméra, elle suit la caméra dans son mouvement. Centrer la skybox sur la caméra permet de simuler des décors très lointains, suffisamment lointain pour qu'on n'ait pas l'illusion de s'en rapprocher en se déplaçant dans la map. De plus, cela évite d'avoir à faire trop de calculs à chaque fois que l'on bouge la caméra. La texture plaquée sur le cube est une texture unique, elle-même découpée en six sous-textures, une par face du cube.
[[File:Skybox example.png|centre|vignette|upright=2|Exemple de Skybox.]]
[[File:Cube mapped reflection example.jpg|vignette|Réflexions calculées par une ''cubemap''.]]
Le ''cube-mapping'' est aussi utilisé pour des reflets. L'idée est de simuler les reflets en plaquant une texture pré-calculée sur l'objet réflecteur. La texture pré-calculée est un dessin de l'environnement qui se reflète sur l'objet, un dessin du reflet à afficher. En la plaquant la texture sur l'objet, on simule ainsi des reflets de l'environnement, mais on ne peut pas calculer d'autres reflets comme les reflets objets mobiles comme les personnages. Et il se trouve que la texture pré-calculée est une ''cubemap''. Pour les environnements ouverts, c'est la ''skybox'' qui est utilisée, ce qui permet de simuler les reflets dans les flaques d'eau ou dans des lacs/océans/autres. Pour les environnements intérieurs, c'est une cubemap spécifique qui utilisée. Par exemple, pour l'intérieur d'une maison, on a une ''cubemap'' par pièce de la maison. Les reflets se calculent en précisant quelle ''cubemap'' appliquer sur l'objet en fonction de la direction du regard.
[[File:Cube map level.png|centre|vignette|Cube map de l'intérieur d'une pièce d'un niveau de jeux vidéo.]]
Toujours est-il que les textures utilisées pour le ''cubemmapping'', appelées des ''cubemaps'', sont en réalité la concaténation de six textures différentes. En mémoire vidéo, la ''cubemap'' est stockée comme six textures les unes à la suite des autres. Lors du rendu, on doit préciser quelle face du cube utiliser, ce qui fait 6 possibilités. On a le même problème qu'avec les niveaux de détail, sauf que ce sont les faces d'une ''cubemap'' qui remplacent les textures de niveaux de détails. L'accès en mémoire doit donc préciser quelle portion de la ''cubemap'' il faut accéder. Et l'accès mémoire se complexifie donc. Surtout que l'accès en question varie beaucoup suivant l'API graphique utilisée, et donc suivant la carte graphique.
Les API 3D assez anciennes ne gérent pas nativement les ''cubemaps'', qui doivent être émulées en logiciel en utilisant six textures différentes. Le pixel shader décide donc quelle ''cubemap'' utiliser, avec quelques calculs sur la direction du regard. L'accès se fait d'une manière assez simple : le shader choisit quelle texture utiliser. Les API 3D récentes gèrent nativement les ''cubemaps''. Dans le cas le plus simple,pour les versions les plus vielles de ces API, les six faces sont numérotées et l'accès à une ''cubemap'' précise quel face utiliser en donnant son numéro. La carte graphique choisit alors automatiquement la bonne texture, mais cela demande de laisser le calcul de la bonne face au pixel shader. D'autres API 3D et cartes graphiques font autrement. Dans les API 3D modenres, les ''cubemap'' sont gérées comme des textures en trois dimensions, adressées avec trois coordonnées u,v,w. La carte graphique utilise ces trois coordonnées de manière à en déduire quelle est la face pertinente, mais aussi les coordonnées u,v dans la texture de la face.
==L'implémentation matérielle du placage de textures==
Pour résumer, la lecture d'un texel demande d'effectuer plusieurs étapes. Dans le cas le plus simple, sans ''mip-mapping'' ou ''cubemapping'', on doit effectuer les étapes suivantes :
* Il faut d'abord normaliser les coordonnées de texture pour qu'elles tombent dans l'intervalle [0,1] en fonction du mode d'adressage désiré.
* Ensuite, les coordonnées u,v doivent être converties en coordonnées entières, ce qui demande une multiplication flottante.
* Enfin, l'adresse finale est calculée à partir des coordonnées entières et en ajoutant l'adresse de base de la texture (et éventuellement avec d'autres calculs arithmétiques suivant le format de la texture).
Tout cela pourrait être fait par le pixel shaders, mais cela implique beaucoup de calculs répétitifs et d'opérations arithmétiques assez lourdes, avec des multiplications flottantes, des additions et des multiplications entières, etc. Faire faire tous ces calculs par les shaders serait couteux en performance, sans compter que les shaders deviendraient plus gros et que cela aurait des conséquences sur le cache d'instruction. De plus, certaines de ces étapes peuvent se faire en parallèle, comme les deux premières, ce qui colle mal avec l'aspect sériel des shaders.
Aussi, les processeurs de shaders incorporent une unité de calcul d'adresse spéciale pour faire ces calculs directement en matériel. L'unité de texture contient au minimum deux circuits : un circuit de calcul d'adresse, et un circuit d'accès à la mémoire. Toute la difficulté tient dans le calcul d'adresse, plus que dans le circuit de lecture. Le calcul d'adresse est conceptuellement réalisé en deux étapes. La première étape qui transforme les coordonnées u,v en coordonnées x,y qui donne le numéro de la ligne et de la colonne du texel dans la texture. La seconde étape prend ces deux coordonnées x,y, l'adresse de la texture, et détermine l'adresse de la tile à lire.
[[File:Unité de texture simple.png|centre|vignette|upright=2|Unité de texture simple]]
===L'implémentation du mip-mapping===
Le ''mip-mapping'' est lui aussi pris en charge par l'unité de calcul d'adresse, car cette technique change l'adresse de base de la texture. La gestion du ''mip-mapping'' est cependant assez complexe. Il est possible de laisser le pixel shader calculer quel niveau de détail utiliser, en fonction de la coordonnée de profondeur z du pixel à afficher. La carte graphique détermine alors automatiquement quelle texture lire, quel niveau de détail, automatiquement. Elle détermine aussi la bonne résolution pour la texture, qui est égal à la résolution de la texture de base, divisée par le niveau de détail. Pour résumer, le niveau de détail est envoyé aux unités de texture, qui s'occupent de calculer l'adresse de base et la résolution adéquates. Quelques calculs arithmétiques simples, donc, qui s'implémentent facilement avec quelques circuits.
Mais une autre méthode laisse la carte graphique déterminer le niveau de détail par elle-même. Dans ce cas, cela demande, outre les deux coordonnées de texture, de calculer la dérivée de ces deux coordonnées dans le sens horizontal et vertical, ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Les quatre dérivées sont les suivantes :
: <math>\frac{du}{dx}</math>, <math>\frac{dv}{dx}</math>, <math>\frac{du}{dy}</math>, <math>\frac{dv}{dy}</math>
Un bon moyen pour obtenir les dérivées demande de regrouper les pixels par groupes de 4 et de faire la différence entre leurs coordonnées de texture respectives. On peut calculer les deux dérivées horizontales en comparant les deux pixels sur la même ligne, et les deux dérivées verticales en comparant les deux pixels sur la même colonne. Mais cela demande de rastériser les pixels par groupes de 4, par ''quads''. Et c'est ce qui est fait sur les cartes graphiques actuelles, qui rastérisent des groupes de 4 pixels à la fois.
[[File:Texture sampler unit with mipmapping.png|centre|vignette|upright=2.0|Unité de texture avec mipmapping.]]
Malheureusement, le calcul exact utilisé pour le choix de la mip-map dépend du GPU considéré et peu de chose est connu quant à ces algorithmes. Il est possible d'inférer le comportement à partir d'observations, mais guère plus. Pour ceux qui veulent en savoir plus, je conseille la lecture de cet article de blog :
* [https://pema.dev/2025/05/09/mipmaps-too-much-detail/ Mipmap selection in too much detail]
===La gestion des accès mémoire===
Enfin, l'unité de texture doit tenir compte du fait que la mémoire vidéo met du temps à lire une texture. En théorie, l'unité de texture ne devrait pas accepter de nouvelle demande de lecture tant que celle en cours n'est pas terminée. Mais faire ainsi demanderait de bloquer tout le pipeline, de l'''input assembler'' au unités de''shaders'', ce qui est tout sauf pratique et nuirait grandement aux performances.
Une solution alternative consiste à mettre en attente les demandes de lectures de texture pendant que la mémoire est occupée. La manière la plus simple d'implémenter des accès mémoire multiples est de les mettre en attente dans une petite mémoire FIFO. Cela implique que les accès mémoire s’exécutent dans l'ordre demandé par le ''shader'' et/ou l'unité de rastérisation, il n'y a pas de réorganisation des accès mémoire ou d’exécution dans le désordre des accès mémoire.
[[File:Texture prefetching.png|centre|vignette|upright=1.5|Accès mémoire simultanés.]]
Évidemment, quand la mémoire FIFO est pleine, le pipeline est alors totalement bloqué. Le rasteriser est prévenu que l'unité de texture ne peut pas accepter de nouvelle lecture de texture. En pratique, la FIFO est généralement d'une taille respectable et permet de mettre en attente beaucoup de demandes de lecture de texture. Il faut de plus noter qu'il y a une FIFO par processeur de ''shader'' sur les cartes graphiques modernes. Quand elle est pleine, le processeur cesse d'exécuter de nouveaux accès mémoire, mais peut continuer à exécuter des ''shaders'' dans les autres unités de calcul, pas besoin de bloquer complétement le pipeline.
===L'intégration du cache de textures===
Il faut noter que les unités de texture incorporent aussi un cache de texture, voire plusieurs. L'intégration des caches de texture avec la mémoire FIFO précédente est quelque peu compliqué, car il faut garantir que les lectures de texture se fassent dans le bon ordre. On ne peut pas exécuter une lecture dans le cache alors que des lectures précédentes sont en attente de lecture en mémoire vidéo. Et cela pose un gros problème : une lecture dans le cache de texture prend quelques dizaines de cycles d'horloge, alors qu'une lecture en mémoire vidéo en prend facilement 400 à 800 cycles, parfois plus. Et cela fait que l'ordre des accès mémoire peut s'inverser.
Prenons par exemple un accès au cache précédé et suivi par deux accès en mémoire vidéo. Le premier démarre au cycle 1, et se termine au cycle numéro 400. L'accès au cache commence au cycle 2 et se termine 20 cycles après, au cycle numéro 22. En clair, la lecture dans le cache s'est terminée avant l'accès mémoire qui le précède. Les textures ne sont donc plus lues dans l'ordre. Et il faut trouver une solution pour éviter cela.
La solution est de retarder les lectures dans le cache tant que tous les accès précédents ne sont pas terminés. Mais pour retarder les lectures en question, il faut d'abord savoir si la lecture atterrit dans le cache ou non, ce qui demande d'accéder au cache. On fait face à un dilemme : on veut retarder les accès au cache, mais les différencier des lectures déclenchant des accès mémoire demande d'accéder au cache en premier lieu. La solution est décrite dans l'article "Prefetching in a Texture Cache Architecture" par Igehy et ses collègues. Elle se base sur deux idées combinées ensemble.
La première idée est de séparer l'accès au cache en deux : une étape qui vérifie si les texels à lire sont dans le cache, et une étape qui accède aux données dans le cache lui-même. Un cache de texture est donc composé de deux circuits principaux. Le premier vérifie la présence des texels dans le cache. Il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Ensuite, en plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
La seconde idée est de retarder l'accès au cache entre les deux phases. La première étape d'un accès mémoire vérifie si la donnée est dans le cache ou non. Puis, on retarde la lecture des données, pour attendre que toutes les lectures précédentes soient terminées. Et enfin, troisième étape : la lecture des texels dans la mémoire cache proprement dite. Les accès mémoire passant par la mémoire vidéo se font de la même manière, à une différence près : la lecture dans le cache est remplacée par la lecture en mémoire vidéo. Tout démarre avec une demande à l'unité de tags, qui vérifie si le texel est dans le cache ou non. Puis on retarde l'accès tant que la mémoire vidéo est occupée, puis on effectue la lecture en mémoire vidéo.
Si ce n'est pas le cas, l'accès mémoire est envoyé à la mémoire vidéo comme précédemment, à savoir qu'il est mis en attente dans une mémoire FIFO, puis envoyé à la mémoire vidéo dès que celle-ci est libre. Mais en sortie de la mémoire, la donnée lue est envoyée dans le cache de texture, par dans l'unité de filtrage. Pour savoir où placer la donnée lue, l'unité de tag a réservé une ligne de cache précise, une adresse bien précise. L'adresse en question est disponible en lisant une autre mémoire FIFO, qui a mis en attente l'adresse en question, en attendant que l'accès mémoire se termine. La donnée est alors écrite dans le cache, puis lue par l'unité de filtrage de textures.
Pour une lecture dans le cache, le déroulement est similaire, mais sans le passage par la mémoire. La lecture fait une demande à l'unité de tag, et celle-ci répond que la donnée est bien dans le cache. Elle place alors l'adresse à lire dans la file d'attente. Une fois que les accès mémoire précédents sont terminés, l'adresse sort de la file d'attente et est envoyée à la mémoire de données. La lecture s'effectue, les texels sont envoyés à l'unité de filtrage de textures. La seule différence avec un ''phased cache'' normal est l'insertion de l'adresse à lire dans une FIFO qui vise à mettre en attente
[[File:Unité de texture avec un cache de texture.png|centre|vignette|upright=2.0|Unité de texture avec un cache de texture]]
Pour résumer, l'implémentation précédente garantit une exécution des lectures dans leur ordre d'arrivée. Et pour cela, elle retarde les lectures dans le cache tant que les lectures en mémoire précédentes ne sont pas terminées. L'accès au cache est plus rapide que l'accès en mémoire vidéo, mais le retard ajouté pour garantir l'ordre des lectures fait que le temps d'accès est très long.
==Le filtrage de textures==
Plaquer des textures sans autre forme de procès ne suffit pas à garantir des graphismes d'une qualité époustouflante. La raison est que les sommets et les texels ne tombent pas tout pile sur un pixel de l'écran : le sommet associé au texel peut être un petit peu trop en haut, ou trop à gauche, etc. Une explication plus concrète fait intervenir les coordonnées de texture. Souvenez-vous que lorsque l'on traduit une coordonnée de texture u,v en coordonnées x,y, on obtient un résultat qui ne tombe pas forcément juste. Souvent, le résultat a une partie fractionnaire. Si celle-ci est non-nulle, cela signifie que le texel/sommet n'est pas situé exactement sur le pixel voulu et que celui-ci est situé à une certaine distance. Concrètement, le pixel tombe entre quatre texels, comme indiqué ci-dessous.
[[File:Filtrage texture.png|centre|vignette|upright=2.0|Position du pixel par rapport aux texels.]]
Pour résoudre ce problème, on doit utiliser différentes techniques d'interpolation, aussi appelées techniques de '''filtrage de texture''', qui visent à calculer la couleur du pixel final en fonction des texels qui l'entourent. Il existe de nombreux types de filtrage de textures, qu'il s'agisse du filtrage linéaire, bilinéaire, trilinéaire, anisotropique et bien d'autres.
Tous ont besoin d'avoir certaines informations qui sont généralement fournies par les circuits de calcul d'adresse. La première est clairement la partie fractionnaire des coordonnées x,y. La seconde est la dérivée de ces deux coordonnées dans le sens horizontal et vertical., ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Toujours est-il que le filtrage de texture est une opération assez lourde, qui demande beaucoup de calculs arithmétiques. On pourrait en théorie le faire dans les pixels shaders, mais le cout en performance serait absolument insoutenable. Aussi, les cartes graphiques intègrent toutes un circuit dédié au filtrage de texture, le ''texture sampler''. Même les plus anciennes cartes graphiques incorporent une unité de filtrage de texture, ce qui nous montre à quel point cette opération est importante.
[[File:Texture unit.png|centre|vignette|upright=2.0|Unité de texture.]]
On peut configurer la carte graphique de manière à ce qu'elle fasse soit du filtrage bilinéaire, soit du filtrage trilinéaire, on peut configurer le niveau de filtrage anisotropique, etc. Cela peut se faire dans les options de la carte graphique, mais cela peut aussi être géré par l'application. La majorité des jeux vidéos permettent de régler cela dans les options. Ces réglages ne concernent pas la texture elle-même, mais plutôt la manière dont l'unité de texture doit fonctionner. Ces réglages sur l''''état de l'unité de texture''' sont mémorisés quelque part, soit dans l'unité de texture elle-même, soit fournies avec la ressource de texture elle-même, tout dépend de la carte graphique. Certaines cartes graphiques mémorisent ces réglages dans les unités de texture ou dans le processeur de commande, et tout changement demande alors de réinitialiser l'état des unités de texture, ce qui prend un peu de temps. D'autres placent ces réglages dans les ressources de texture elles-mêmes, ce qui rend les modifications de configuration plus rapides, mais demande plus de circuits. D'autres cartes graphiques mélangent les deux options, certains réglages étant globaux, d'autres transmis avec la texture. Bref, difficile de faire des généralités, tout dépend du matériel et le pilote de la carte graphique cache tout cela sous le tapis.
Maintenant que cela est dit, voyons quelles sont les différentes méthodes de filtrage de texture et comment la carte graphique fait pour les calculer.
===Le filtrage au plus proche===
La méthode de filtrage la plus simple consiste à colorier avec le texel le plus proche. Cela revient tout simplement à ne pas tenir compte de la partie fractionnaire des coordonnées x,y, ce qui est très simple à implémenter en matériel. C'est ce que l'on appelle le '''filtrage au plus proche''', aussi appelé ''nearest filtering''.
Autant être franc, le résultat est assez pixelisé et peu agréable à l’œil. Par contre, le résultat est très rapide à calculer, vu qu'il ne demande aucun calcul à proprement parler. Elle ne fait pas appel à la parti fractionnaire des coordonnées entières de texture, ni aux dérivées de ces coordonnées. On peut combiner cette technique avec le mip-mapping, ce qui donne un résultat bien meilleur, bien que loin d'être satisfaisant. Au passage, toutes les techniques de filtrage de texture peuvent se combiner avec du mip-mapping, certaines ne pouvant pas faire sans.
[[File:Interpolation-nearest.svg|centre|vignette|Filtrage de texture au plus proche.]]
===Le filtrage linéaire===
Le filtrage le plus simple est le '''filtrage linéaire'''. Il effectue une interpolation linéaire entre deux mip-maps, deux niveaux de détails. Pour comprendre l'idée, nous allons prendre une situation très simple, avec une texture carrée de 512 texels de côté. Le mip-mapping crée plusieurs textures : une de 256 texels de côté, une de 128 texels, une de 64, etc. Maintenant, la texture est sur un objet à une certaine distance de l'écran, vu de face. Le résultat est qu'elle correspond à l'écran à un carré de 300 pixels de côté (pas d'erreur : pixels, pas texels). Dans ce cas, la texture se trouve entre deux mip-maps : celle de 512 pixels de côté, celle de 256. Laquelle choisir ? Le filtrage au plus proche prend la texture de 512 pixels de côté. Le filtrage linéaire lui, fait autrement.
Vu que la texture est entre deux mip-maps, l'idée est de prendre le texel au plus proche dans chaque texture et de faire une sorte de moyenne appelée l'interpolation linéaire. L'interpolation par du principe que la couleur varie entre les deux texels en suivant une fonction affine, illustrée ci-dessous. Ce ne serait évidemment pas le cas dans le monde réel, mais on supposer cela donne une bonne approximation de ce à quoi ressemblerait une texture à plus haute résolution. On peut alors calculer la couleur du pixel par une simple moyenne pondérée par la distance. Le résultat est que les transitions entre deux niveaux de détails sont plus lisses, moins abruptes.
[[File:Lin interp -é.png|centre|vignette|upright=2.0|Interpolation linéaire.]]
===Le filtrage bilinéaire===
Le filtrage bilinéaire effectue une sorte de moyenne pondérée des quatre texels les plus proches du pixel à afficher. Pour cela, rappelez-vous ce qui a été dit plus haut : les coordonnées x,y d'un pixel ont une partie entière et une partie fractionnaire. Le filtrage au plus proche élimine les parties fractionnaires, ce qui donne une coordonnée x,y. Avec le filtrage bilinéaire, on prend les texels de coordonnées (x,y) ; (x+1,y) ; (x,y+1) ; (x+1,y+1), le pixel étant entre ces 4 texels.
Mais le filtrage ne fait pas qu'une simple moyenne, il prend en compte les parties fractionnaires pour faire la moyenne. En effet, le pixel n'est pas au milieu du carré de texel, il est quelque part mais est souvent plus proche d'un texel que des autres. Et il faut donc pondérer la moyenne par les distances aux 4 texels. Pour cela, la moyenne est calculée à partir d'interpolations linéaires. Avec 4 pixels, nous allons devoir calculer la couleur de deux points intermédiaires. La couleur de ces deux points se calcule par interpolation linéaire, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.
[[File:Bilin3.png|centre|vignette|upright=2|Filtrage bilinéaire de texture.]]
Le circuit qui permet de faire l'interpolation bilinéaire est particulièrement simple. On trouve un circuit de chaque pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu, et un pour la transparence. Chacun de ces circuit est composé de sous-circuits chargés d'effectuer une interpolation linéaire, reliés comme suit.
[[File:Texture sampler unit.png|centre|vignette|Unité de filtrage bilinéaire.]]
Vous noterez que le filtrage bilinéaire accède à 4 pixels en même temps. Fort heureusement, les textures sont stockées de manière à ce qu'on puisse charger les 4 pixels en une fois, comme on l'a vu plus haut. Le filtrage bilinéaire a de fortes chances que les 4 pixels filtrés soient dans la même ''tile'', la seule exception étant quand ils sont tout juste sur le bord d'une ''tile''.
: La console de jeu Nintendo 64 n'utilise que trois pixels au lieu de quatre dans son interpolation bilinéaire, qui en devient une interpolation quasi-bilinéaire. La raison derrière ce choix est une question de performances, comme beaucoup de décisions de ce genre. Le résultat est un rendu imparfait de certaines textures.
===Le filtrage trilinéaire===
Avec le filtrage bilinéaire, des discontinuités apparaissent sur certaines surfaces. Par exemple, pensez à une texture de sol : elle est appliquée plusieurs fois sur toute la surface du sol. A une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512*512 à 256*256, ce qui est visible pour un joueur attentif. De telles transitions sont lissées grâce au filtrage linéaire, il n'y a plus qu'à le combiner avec le filtrage bilinéaire. Rien d’incompatible : le premier filtre l'intérieur d'une mip-map, le second combine deux mip-maps.
Le filtrage trilinéaire prend les deux mip-maps les plus proches, fait un filtrage bilinéaire avec chacune, puis fait une « une moyenne » pondérée entre les deux résultats. Le circuit de filtrage trilinéaire existe en plusieurs versions. La plus simple, illustrée ci-dessous, effectue deux filtrages bilinéaires en parallèle, dans deux circuits séparés, puis combine leurs résultats avec un circuit d'interpolation linéaire. Mais ce circuit nécessite de charger 8 texels simultanément. Qui plus est, ces 8 texels ne sont pas consécutifs en mémoire, car ils sont dans deux niveaux de détails/mip-maps différents.
[[File:Parallel trilinear filtering.png|centre|vignette|upright=2.0|Unité de filtrage trilinéaire parallèle.]]
Vu qu'on lit des texels dans deux mip-maps, les texels sont lus en deux fois : 4 texels provenant de la première mip-map, suivis par les 4 texels de l'autre mip-map. Les 4 premiers texels doivent donc être mis en attente dans des registres, en attendant que les 4 autres arrivent. Une amélioration du circuit précédent gère cela en ajoutant des registres. Il lit les 4 premiers texels, les filtre avec une interpolation bilinéaire, et mémorise le résultat dans un registre. Puis, il lit les 4 autres texels, les filtre, et met le résultat dans un second registre. A ce moment là, un circuit d'interpolation linéaire finit le travail. On économise donc un circuit d'interpolation bilinéaire, sans que les performances soient trop impactées.
[[File:Filtrage trilineaire.png|centre|vignette|upright=1.0|Unité de filtrage trilineaire série.]]
Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le temps où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. À chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients, et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.
===Le filtrage anisotrope===
D'autres artefacts peuvent survenir lors de l'application d'une texture, la perspective pouvant déformer les textures et entraîner l'apparition de flou. La raison à cela est que les techniques de filtrage de texture précédentes partent du principe que la texture est vue de face. Prenez une texture carrée, par exemple. Vue de face, elle ressemble à un carré sur l'écran. Mais tournez la caméra, de manière à voir la texture de biais, avec un angle, et vous verrez que la forme de la texture sur l'écran est un trapèze, pas un carré. Cette déformation liée à la perspective n'est pas prise en compte par les méthodes de filtrage de texture précédentes. Pour le dire autrement, les techniques de filtrage précédentes partent du principe que les 4 texels qui entourent un pixel forment un carré, ce qui est vrai si la texture est vue de face, sans angle, mais ne l'est pas si la texture n'est pas perpendiculaire à l'axe de la caméra. Du point de vue de la caméra, les 4 texels forment un trapèze d'autant moins proche d'un carré que l'angle est grand.
Pour corriger cela, les chercheurs ont inventé le '''filtrage anisotrope'''. En fait, je devrais plutôt dire : LES filtrages anisotropes. Il en existe un grand nombre, dont certains ne sont pas utilisés dans les cartes graphiques actuelles, soit car ils trop gourmand en accès mémoires et en calculs pour être efficaces, soit car ils ne sont pas pratiques à mettre en œuvre. Il est très difficile de savoir quelles sont les techniques de filtrage de texture utilisées par les cartes graphiques, qu'elles soient récentes ou anciennes. Beaucoup de ces technologies sont brevetées ou gardées secrètes, et il faudrait vraiment creuser les brevets déposés par les fabricants de GPU pour en savoir plus. Les algorithmes en question seraient de plus difficiles à comprendre, les méthodes mathématiques cachées derrière ces méthodes de filtrage n'étant pas des plus simple.
[[File:Anisotropic filtering en.png|centre|vignette|upright=2|Exemple de filtrage anisotrope.]]
==La compression de textures==
Les textures les plus grosses peuvent aller jusqu'au mébioctet, ce qui est beaucoup. Pour limiter la casse, les textures sont compressées. La '''compression de texture''' réduit la taille des textures, ce qui peut se faire avec ou sans perte de qualité. Elle entraîne souvent une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande. Mais il s'agit là d'une technique très simple, beaucoup plus simple que les techniques que nous allons voir dans cette section. Nous allons voir quelque algorithmes de compression de textures de complexité intermédiaire, mais n'allons pas voir l'état de l'art. Il existe des formats de texture plus récents que ceux qui nous allons aborder, comme l{{'}}''Ericsson Texture Compression'' ou l{{'}}''Adaptive Scalable Texture Compression'', plus complexes et plus efficaces.
Notons que les textures sont compressées dans les fichiers du jeu, mais aussi en mémoire vidéo. Les textures sont décompressées lors de la lecture. Pour cela, la carte graphique contient alors un circuit, capable de décompresser les textures lorsqu'on les lit en mémoire vidéo. Les cartes graphiques supportent un grand nombre de formats de textures, au niveau du circuit de décompression. Du fait que les textures sont décompressées à la volée, les techniques de compression utilisées sont assez particulières. La carte graphique ne peut pas décompresser une texture entière avant de pouvoir l'utiliser dans un ''pixel shader''. A la place, on doit pouvoir lire un morceau de texture, et le décompresser à la volée. On ne peut utiliser les méthodes de compression du JPEG, ou d'autres formats de compression d'image. Ces dernières ne permettent pas de décompresser une image morceau par morceau.
Pour permettre une décompression/compression à la volée, les textures sont des textures tilées, généralement découpées en tiles de 4 * 4 texels. Les ''tiles'' sont compressées indépendamment les unes des autres. Et surtout, avec ou sans compression, la position des tiles en mémoire ne change pas. On trouve toujours une tile tous les T octets, peu importe que la tile soit compressée ou non. Par contre, une tile compressée n'occupera pas T octets, mais moins, là où une tile compressée occupera la totalité des T octets. En clair, compresser une tile fait qu'il y a des vides entre deux tiles dans al mémoire vidéo, mais ne change rien à leur place en mémoire vidéo qui est prédéterminée, peu importe que la texture soit compressée ou non. L'intérêt de la compression de textures n'est pas de réduire la taille de la texture en mémoire vidéo, mais de réduire la quantité de données à lire/écrire en mémoire vidéo. Au lieu de lire T octets pour une tile non-compressée, on pourra en lire moins.
===La palette indicée et la technique de ''Vector quantization''===
La technique de compression des textures la plus simple est celle de la '''palette indicée''', que l'on a entraperçue dans le chapitre sur les cartes d'affichage. La technique de '''''vector quantization''''' peut être vue comme une amélioration de la palette, qui travaille non pas sur des texels, mais sur des ''tiles''. À l'intérieur de la carte graphique, on trouve une table qui stocke toutes les ''tiles'' possibles. Chaque ''tile'' se voit attribuer un numéro, et la texture sera composé d'une suite de ces numéros. Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l’embarqué utilisent ce genre de compression.
===Les algorithmes de ''Block Truncation coding''===
La première technique de compression élaborée est celle du '''''Block Truncation Coding''''', qui ne marche que pour les images en niveaux de gris. Le BTC ne mémorise que deux niveaux de gris par ''tile'', que nous appellerons couleur 1 et couleur 2, les deux niveaux de gris n'étant pas le même d'une ''tile'' à l'autre. Chaque pixel d'une ''tile'' est obligatoirement colorié avec un de ces niveaux de gris. Pour chaque pixel d'une ''tile'', on mémorise sa couleur avec un bit : 0 pour couleur 1, et 1 pour couleur 2. Chaque ''tile'' est donc codée par deux entiers, qui codent chacun un niveau de gris, et une suite de bits pour les pixels proprement dit. Le circuit de décompression est alors vraiment très simple, comme illustré ci-dessous.
[[File:Block Truncation coding.jpg|centre|vignette|upright=2.0|Block Truncation coding.]]
La technique du BTC peut être appliquée non pas du des niveaux de gris, mais pour chaque composante Rouge, Vert et Bleu. Dans ces conditions, chaque ''tile'' est séparée en trois sous-''tiles'' : un sous-bloc pour la composante verte, un autre pour le rouge, et un dernier pour le bleu. Cela prend donc trois fois plus de place en mémoire que le BTC pur, mais cela permet de gérer les images couleur.
===Le format de compression S3TC / DXTC===
L'algorithme de '''Color Cell Compression''', ou CCC, améliore le BTC pour qu'il gère des couleurs autre que des niveaux de gris. Ce CCC remplace les deux niveaux de gris par deux couleurs. Une ''tile'' est donc codée avec un entier 32 bits par couleur, et une suite de bits pour les pixels. Le circuit de décompression est identique à celui utilisé pour le BTC.
[[File:Color Cell Compression.jpg|centre|vignette|Color Cell Compression.]]
[[File:Dxt1-memory-layout.png|vignette|Dxt1 et ''color cell compression''.]]
Le format de compression de texture utilisé de base par Direct X, le DXTC, est une version amliorée de l'algorithme précédent. Il est décliné en plusieurs versions : DXTC1, DXTC2, etc. La première version du DXTC est une sorte d'amélioration du CCC : il ajoute une gestion minimale de transparence, et découpe la texture à compresser en ''tiles'' de 4 pixels de côté. La différence, c'est que la couleur finale d'un texel est un mélange des deux couleurs attribuée au bloc. Pour indiquer comment faire ce mélange, on trouve deux bits de contrôle par texel.
Si jamais la couleur 1 < couleur2, ces deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (2 * Couleur1 + Couleur2) / 3
* 11 = (Couleur1 + 2 * Couleur2) / 3
Sinon, les deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (Couleur1 + Couleur2) / 2
* 11 = Transparent
[[File:DXTC.jpg|centre|vignette|DXTC.]]
Le circuit de décompression du DXTC ressemble alors à ceci :
[[File:Circuit de décompression du DXTC.jpg|centre|vignette|upright=2.0|Circuit de décompression du DXTC.]]
===Les format DXTC 2, 3, 4 et 5 : l'ajout de la transparence===
Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3, lui-même replacé par le DXT4 et par le DXT5. Dans le DXT3, la transparence fait son apparition. Pour cela, on ajoute 64 bits par ''tile'' pour stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1.
[[File:Dxt23-memory-layout.png|centre|vignette|Dxt 2 et 3.]]
Dans le DXT4 et le DXT5, la méthode utilisée pour compresser les couleurs l'est aussi pour les valeurs de transparence. L'information de transparence est stockée par un en-tête contenant deux valeurs de transparence, le tout suivi d'une matrice qui attribue trois bits à chaque texel. En fonction de la valeur des trois bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1.
[[File:Dxt45-memory-layout.png|centre|vignette|Dxt 4 et 5.]]
===Le format de compression PVRTC===
Passons maintenant à un format de compression de texture un peu moins connu, mais pourtant omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle travaille surtout dans les cartes graphiques embarquées. Ses cartes se trouvent notamment dans l'ipad, l'iPhone, et bien d'autres smartphones actuels.
Avec le PVRTC, les textures sont encore une fois découpées en ''tiles'' de 4 texels par 4, mais la ressemblance avec le DXTC s’arrête là. Chacque ''tile'' est codée avec :
* une couleur codée sur 16 bits ;
* une couleur codée sur 15 bits ;
* 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
* et un bit de modulation, qui permet de configurer l’interprétation des bits de mélange.
Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4*4.
==Annexe : le ''normal-mapping'' hardware==
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, parlons un peu du ''normal mapping'' et de son implémentation dans les unités de texture. Pour rappel, 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. Et pour comprendre quel est le rapport avec les textures, nous allons devoir faire quelques rappels sur l'éclairage par pixel.
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
L'éclairage se fait en utilisant de nombreux calculs, qu'on a détaillé dans le chapitre sur les bases du rendu 3D. Ceux-ci utilisent la normale d'un sommet, à savoir un vecteur orienté à la verticale de la surface. En théorie, il y a une normale par sommet, ce qui fait que les calculs d'éclairage doivent se faire au niveau géométrique, avant la rastérisation. Mais pour obtenir un éclairage de meilleure qualité, il y a des techniques qui calcule l'éclairage d'une scène 3D pixel par pixel.
Une première technique d'éclairage par pixel interpole les normales lors de l'étape de rastérisation. On obtient alors un éclairage de Phong. Une autre solution est celle du ''normal mapping''. Le '''''normal mapping''''' précalcule les normales d'une surface dans une texture, appelée la ''normal map''. L'éclairage est alors réalisé par un pixel shader, qui lit les normales depuis cette texture et fait les calculs d'éclairage avec.
===Les ''normal maps'' sont des textures===
Notons que l'accès à la ''normal map'' se fait comme pour n'importe quelle texture : les texels sont lus en mémoire vidéo, puis le filtrage de texture est appliqué, et enfin le tout est envoyé au ''pixel shader''. En théorie, le filtrage effectue une sorte d'interpolation des normales automatique, ce qui permet de trouver la normale à un pixel précis, même s'il n'est pas sur un texel.
La ''normal map'' est une texture, donc. Mais ce n'est pas une texture comme une autre. Elle mémorise un vecteur pour chaque texel, pas une couleur. Il est possible d'utiliser les formats de textures vus plus haut, même si cela ne donne pas de bons résultats. Après tout, un vecteur est codé par trois coordonnées x,y,z, il est possible de coder chacune avec un octet et de packager cela dans une texture RGB classique. La coordonnée x va dans la composante Rouge, la coordonnée Y dans la composante Bleu, et la coordonnée z dans la composante z.
Le problème est que le rendu final n'est pas très beau. Et les opérations de filtrage ne donnent pas de très bons résultats. Pour remédier à ces problèmes, des formats de texture spécialisés pour les ''normal map'' ont été inventés. Le premier d'entre eux est le '''3Dc''', aussi appelé BC5 pour ''Block Compression 5''. Il utilise un octet par texel, au lieu de trois avec une texture RGB normale non-compressée.
===Le format de texture 3Dc===
Avec le 3Dc, les trois coordonnées des vecteurs ne sont pas mémorisées, seules deux d'entre elles le sont. En effet, les normales sont des vecteurs ''normalisés'', c'est à dire que leur longueur vaut 1, par construction. Vu que la taille est connue à l'avance, on peut en déduire la coordonnée z à partir des coordonnées x et y, avec l'usage du théorème de Pythagore. Par définition, <math>z^2 = 1 - x^2 - y^2</math>. Le calcul peut être fait dans le ''pixel shader'' sans problème.
Pour le reste, la compression de la ''normal map'' ressemble un peu à celle des textures. La ''normal map'' est découpée en blocs de 4 par 4 texels de côté, chacune étant encodée en tenant compte de certaines redondances. Dans un bloc, les coordonnées x sont dans un certain intervalle, allant de <math>x_{min}</math> à <math>x_{max}</math>, idem pour la coordonnée y qui est dans l'intervalle <math>y_{min}</math>, <math>y_{max}</math>. L'intervalle <math>x_{min}</math> à <math>x_{max}</math> est coupé en 8 parts égales, ce qui fait 8 valeurs possibles dans cet intervalle, chacune correspondant à une valeur x possible pour une normale du bloc. La normale est donc encodée en précisant quelle valeur est la bonne, en utilisant un indice de 3 bits pour cela. La valeur exacte de la coordonnée x se calcule à partir de l'indice comme suit :
: <math>x = x_{min} + \left( \frac{x_{min} - x_{min}}{8} \right) \times \text{indice} </math>
Pour encoder un bloc de 4 pr 4 normales, il faut donc :
* Un octet pour chaque coordonnée <math>x_{min}</math>, <math>x_{max}</math>, <math>y_{min}</math>, <math>y_{max}</math>.
* 6 bits par normale : 3 pour encoder le décalage de la coordonnée x, 3 pour la coordonnée y.
Le tout permet d'encoder un bloc de 16 normales en seulement 128 bits, soit un octet par normale. Le ''pixel shader'' se débrouille pour décompresser un bloc. Il effectue notamment le calcul du dessus, pour retrouver la coordonnée x. Les calculs étant particulièrement simples, le cout en performance est très faible. Et ce d'autant plus qu'ils demandent juste de faire des additions et des multiplications entières, que l'unité scalaire peut faire en parallèle d'autres opérations plus gourmandes. Et le cout en calculs est de toute façon compensé par l'économie de mémoire et de bande passante lié à la compression : diviser la taille des données par trois, ca a un sacré impact !
==Annexe : les ''shadowmap'' hardware==
Les anciens GPU, notamment la Geforce FX, avaient des fonctionnalités spécifiques pour le calcul des ombres. Dans la plupart des jeux vidéos de l'époque, et même de maintenant, les ombres sont calculées avec la technique des ''shadowmap''. L'idée est assez simple sur le principe : un pixel est dans l'ombre quand il est invisible depuis une source de lumière.
L'idée est que le rendu est réalisé en plusieurs passes, avec une passe par source de lumière et une passe finale pour calculer l'image finale. Nous allons expliquer la technique avec une seule source de lumière, et allons utiliser l'exemple de la scène ci-dessous.
[[File:7fin.png|centre|vignette|Scène 3D d'exemple.]]
===La technique du ''shadowmapping''===
[[File:2shadowmap.png|vignette|Résultat de la première passe : ''shadowmap''..]]
La première passe rend l'image depuis le point de vue de la source de lumière. Cette première passe ne rend pas les couleurs de la scène, elle ne s'intéresse qu'à la profondeur des pixels. Le résultat est que l'image ne rend que le tampon de profondeur. Celui-ci est ensuite réutilisé comme texture pour la passe suivante. La texture en question est appelée la '''''shadownmap'''''.
La perspective utilisée, ainsi que le ''view frustrum'', dépend de la source de lumière. Pour une source de lumière qui émet un cône de lumière, le ''view frustrum'' de l'image rendue doit contenir tout le cone de lumière, et doit coller le plus possible à celui-ci. Pour une source directionnelle, comme le soleil, une perspective orthographique est utilisée.
La seconde passe rend l'image du point de vue de la caméra, pour rendre l'image finale. Elle rend l'image finale, qui est composée de pixels, chacun ayant une position à l'écran x,y, et une profondeur z. Les coordonnées sont transformées pour obtenir la position de ce pixel depuis le point de vue de la caméra. Une simple multiplication de matrice suffit, rien de bien compliqué, un shader peut le faire.
[[File:5failed.png|vignette|Résultat du test des comparaisons.]]
Après cette étape, on a alors les coordonnées x,y,z de ce pixel du point de vue de la caméra, et la ''shadowmap''. Il est alors possible d'accéder à la ''shadowmap'' au même endroit, à la même place que le pixel testé, aux mêmes coordonnées x,y. Si la profondeur du pixel est supérieure à celle de la shadowmap au même endroit, alors le pixel est situé derrière la surface visible, donc est dans l'ombre. Sinon, il n'est pas dans l'ombre. Le même procédé est répété sur chaque pixel de l'écran.
===Les optimisations hardware du ''shadowmapping''===
La technique des ''shadowmap'' demande donc de calculer une texture ''shadowmap'', puis de lire celle-ci et de faire des comparaisons de profondeur. Les GPU comme la Geforce FX intégraient du matériel dans les unités de texture pour faciliter ce travail. Les unités de texture pouvaient lire les ''shadowmap'', et faire la comparaison de profondeur toutes seules, elles avaient des circuits pour. Il suffisait de leur fournir le pixel à tester, ses coordonnées x,y,z, et l'adresse de la ''shadowmap''. Les unités de texture renvoyaient alors un résultat valant 0 ou 1 : 1 si le pixel est dans l'ombre, 0 sinon.
Elles pouvaient même effectuer du filtrage de texture sur les ''shadowmap''. Mais le filtrage était différent de celui utilisé sur les autres textures : moyenner des valeurs de profondeur ne marche pas bien. Elles utilisaient des techniques de filtrage différentes : elles faisaient les tests de comparaison, puis faisaient la moyenne des résultats. Ainsi, pour du filtrage bilinéaire, elles lisaient 4 texels dans la ''shadowmap'', puis faisaient 4 tests de comparaison, et moyennaient les 4 résultats.
==Annexe : les textures virtuelles==
Les '''textures virtuelles''' sont une optimisation des textures normales, qui visent à accélérer le rendu de terrains de grande taille. Imaginez par exemple un monde assez ouvert, comme un environnement en forêt ou en montagne, avec une grande distance de visibilité. Avec de tels terrains, le "sol" est recouvert par une texture de sol unique qui recouvre tout le terrain. Elle ne se répète pas, est de très grande taille, et peut parfois recouvrir toute la map ! Mais il n'y a pas assez de mémoire vidéo pour mémoriser la texture toute entière. La seule solution est la suivante : une partie de la texture est placée en mémoire vidéo, le reste est soit placé en mémoire RAM ou sur le disque dur.
Pour cela, le moteur de jeu utilise une optimisation ingénieuse, basée sur une observation assez basique : une bonne partie de la texture est visible, mais le reste est caché par des arbres, des habitations ou d'autres obstacles. Une optimisation possible de ne garder en mémoire vidéo que les portions visibles de la texture, pas les portions cachées. Une autre optimisation mélange textures virtuelles et ''mip-mapping''. L'idée est que pour les portions lointaines d'une texture, la texture utilisée est une ''mip-map'' de basse résolution. L'idée est alors de ne charger que la ''mip-map'' adéquate, pas les autres niveaux de détail. En clair, la texture de base n'est pas chargée en mémoire vidéo, mais la ''mip-map'' basse résolution l'est.
===Une texture à deux niveaux===
L'implémentation des textures virtuelles découpe les méga-textures en ''tiles'', en morceaux rectangulaires de taille modeste. En clair, le terrain est découpé en morceau rectangulaires/carrés. Seules les tiles nécessaires sont chargées en mémoire vidéo, pas les autres. Par exemple, les ''tiles'' non-visibles ne sont pas placées en mémoire vidéo, seules les ''tiles'' visibles le sont. De même, il y a une ''tile'' par niveau de mip-map : seul la tile correspondant le niveau adéquat est en mémoire vidéo, les autres niveaux de détail ne sont pas chargés. On peut faire une analogie avec la mémoire virtuelle, où les données sont découpées en pages, qui sont chargées en mémoire RAM à la demande, suivant les besoins, les données pouvant être swappées sur le disque dur si elles sont peu utilisées. Sauf qu'ici, il s'agit de textures qui sont découpées en pages chargées à la demande en mémoire vidéo, depuis la RAM système.
Une texture virtuelle est en réalité un système à deux niveaux : une liste de ''tiles'' et les ''tiles'' elles-mêmes. La liste de ''tiles'' est appelée un '''atlas de texture''', c'est un peu l'équivalent de la ''tilemap'' pour le rendu 2D. Rendre une texture demande de calculer quelle ''tile'' contient le texel à afficher, consulter la ''tile'' en question, puis récupérer le texel adéquat dans cette ''tile''. La ''tile'' est donc une texture, mais la texture à charger est choisie parmi un ensemble, qui est ici l'atlas de texture.
===L'implémentation : logicielle versus matérielle===
Les textures virtuelles ont été utilisées pour la première fois par les jeux Rage 1 et 2 d'IdSoftware, et quelques jeux ultérieurs comme DOOM 2016. IdSoftware les appelait des '''''mega-textures'''''. L'optimisation permettait des gains en performance assez impressionnants. Le jeu Rage 1 utilisait une texture carrée unique de 128k pixels de côté pour rendre le terrain. En théorie, une telle texture devrait prendre 64 giga-octets, mais le jeu tournait correctement avec 512 méga-octets de RAM, poussivement avec seulement 256 méga-octets de RAM.
De nos jours, les textures virtuelles sont supportées par beaucoup de jeux vidéos, les moteurs les plus courants gèrent de telles textures de manière logicielles. Mais quelques GPU récents supportent les textures virtuelles. Sur les GPU récents, l'atlas de texture est géré nativement par le matériel. Le GPU choisit quelle ''tile'', quelle texture choisir pour rendre le texel adéquat. Pour cela, le GPU calcule quelle ''tile'' charger, consulte l'atlas de texture, et lit la texture de ''tile'' adéquate.
Mais l'implémentation sur les GPU récents a de nombreuses limitations. La limitation la plus importante est que la taille des textures virtuelles ne peut pas dépasser la taille d'une texture normale, soit 32768 pixels de côté pour une texture carrée environ sur les GPU de 2020. De plus, le chargement d'une ''tile'' est très lent. En clair, dès qu'on veut changer de niveau de mip-map pour une tile, ou dès qu'une tile devient visible, le chargement de la tile peut facilement prendre plusieurs centaines de millisecondes. Le filtrage de texture est très complexe avec des textures virtuelles, ce qui fait que le filtrage de texture virtuelle est souvent soumis à des limitations que les textures normales n'ont pas, notamment pour le filtrage anisotropique.
{{NavChapitre | book=Les cartes graphiques
| prev=Le rasterizeur
| prevText=Le rasterizeur
| next=Les Render Output Target
| nextText=Les Render Output Target
}}{{autocat}}
71xv0tu1pfyplfkw72ss48moc97p2wa
763412
763411
2026-04-10T17:52:22Z
Mewtow
31375
/* Le format de texture 3Dc */
763412
wikitext
text/x-wiki
[[File:Texture mapping.png|vignette|''Texture mapping'']]
Les '''textures''' sont des images que l'on va plaquer sur la surface d'un objet, du papier peint en quelque sorte. Les cartes graphiques supportent divers formats de textures, qui indiquent comment les pixels de l'image sont stockés en mémoire : RGB, RGBA, niveaux de gris, etc. Une texture est donc composée de "pixels", comme toute image numérique. Pour bien faire la différence entre les pixels d'une texture, et les pixels de l'écran, les pixels d'une texture sont couramment appelés des ''texels''.
==Le placage de textures inverse==
Pour rappel, plaquer une texture sur un objet consiste à attribuer un texel à chaque sommet, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture.
Dans les faits, on n'utilise pas de coordonnées entières de ce type. Les coordonnées de texture sont deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. Le nom donnée à cette technique de description des coordonnées de texture s'appelle l''''''UV Mapping'''''.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Les API 3D modernes gèrent des textures en trois dimensions, ce qui ajoute une troisième coordonnée de texture notée w. Dans ce qui va suivre, nous allons passer les textures en trois dimensions sous silence. Elles ne sont pas très utilisées, la quasi-totalité des jeux vidéo et applications 3D utilisant des textures en deux dimensions. Par contre, le matériel doit gérer les textures 3D, ce qui le rend plus complexe que prévu. Il faut ajouter quelques circuits pour, de quoi gérer la troisième coordonnée de texture, etc.
Lors de la rastérisation, chaque fragment se voit attribuer un sommet, et donc la coordonnée de texture qui va avec. Si un pixel est situé pile sur un sommet, la coordonnée de texture de ce sommet est attribuée au pixel. Si ce n'est pas le cas, la coordonnée de texture finale est interpolée à partir des coordonnées des trois sommets du triangle rastérisé. L'interpolation en question a lieu dans l'étape de rastérisation, comme nous l'avons vu dans le chapitre précédent. Le fait qu'il y ait une interpolation fait que les coordonnées du pixel gagent à être des nombres flottants. On pourrait faire une interpolation avec des coordonnées de texture entières, mais les arrondis et autres imprécisions de calcul donneraient un résultat graphiquement pas terrible, et empêcheraient d'utiliser les techniques de filtrage de texture que nous verrons dans ce chapitre.
À partir de ces coordonnées de texture, la carte graphique calcule l'adresse du texel qui correspond, et se charge de le lire. Et toute la magie a lieu dans ce calcul d'adresse, qui part de coordonnées de texture flottante, pour arriver à une adresse mémoire. Le calcul de l'adresse du texel se fait en plusieurs étapes, que nous allons voir ci-dessous. La première étape convertit les coordonnées flottantes en coordonnées entières, qui disent à quel ligne et colonne se trouve le texel voulu dans la texture. L'étape suivante transforme ces coordonnées x,y entières en adresse mémoire.
===La normalisation des coordonnées===
J'ai dit plus haut que les coordonnées de texture sont des coordonnées flottantes, comprises entre 0 et 1. Mais il faut savoir que les pixels shaders peuvent modifier celles-ci pour mettre en œuvre certains effets graphiques. Et le résultat peut alors se retrouver en-dehors de l'intervalle 0,1. C'est quelque chose de voulu et qui est traité par la carte graphique automatiquement, sans que ce soit une erreur. Au contraire, la manière dont la carte graphique traite cette situation permet d'implémenter des effets graphiques comme des textures en damier ou en miroir.
[[File:Clamp tile.jpg|vignette|Clamp tile]]
Il existe globalement trois méthodes très simples pour gérer cette situation, qui sont appelés des '''modes d'adressage de texture'''.
* La première méthode est de faire en sorte que le résultat sature. Si une coordonnée est inférieur à 0, alors on la remplace par un zéro. Si elle est supérieure à 1, on la ramène à 1. Avec cette méthode, tout se passe comme si les bords de la texture étaient étendus et remplissaient tout l'espace autour de la texture. Le tout est illustré ci-dessous. Ce mode d'accès aux textures est appelé le '''''clamp'''''.
* Une autre solution retire la partie entière de la coordonnée, elle coupe tout ce qui dépasse 1. Pour le dire autrement, elle calcule le résultat modulo 1 de la coordonnée. Le résultat est que tout se passe comme si la texture était répétée à l'infini et qu'elle pavait le plan.
* Une autre méthode remplit les coordonnées qui sortent de l’intervalle 0,1 avec une couleur préétablie, configurée par le programmeur.
===La conversion des coordonnées de textures flottantes en adresse mémoire===
Une fois la normalisation effectuée, les coordonnées de texture sont utilisées pour lire le texel voulu. Pour cela, les coordonnées de texte sont transformées en adresse mémoire, adresse qui pointe sur le texel ayant ces cordonnées. Pour cela, la première étape est de transformer les coordonnées flottantes u,v en coordonnées entières x,y qui pointent sur un texel. Pour cela, il suffit de multiplier les coordonnées flottantes u,v par la résolution de la texture accédée. Pour un écran de résolution <math>\text{height,width}</math>, le calcul est le suivant :
: <math>x = u \times \text{width}</math>
: <math>y = v \times \text{height}</math>
Le résultat est un nombre avec une partie entière et une partie fractionnaire. La partie entière des deux coordonnées donne la position x,y voulue, et la partie fractionnaire est conservée pour le filtrage de textures, mais passons cela sous silence pour le moment.
La seconde étape prend les coordonnées entières x,y et calcule l'adresse mémoire du texel. L'adresse dépend de la position de la texture en mémoire, précisément de son début, son premier texel, mais aussi de la position du texel par rapport au début de la texture. Et calculer cette position intra-texture dépend de la manière dont les texels sont stockés en mémoire.
====Les textures naïves====
Les programmeurs qui lisent ce cours s'attendent certainement à ce que la texture soit stockée en mémoire ligne par ligne, ou colonne par colonne. Cela veut dire que le premier pixel en partant d'en haut à gauche est stocké en premier, puis celui immédiatement à sa droite, puis celui encore à droite, et ainsi de suite. Une fois qu'on arrive à la fin d'une ligne, on passe à la ligne suivante, en-dessous. Cette organisation ligne par ligne s'appele l'organisation '''''row major order'''''. On peut faire pareil, mais colonne par colonne, ce qui donne le '''''column major order'''''.
[[File:Speicheranordnung Feld.svg|centre|vignette|upright=2|Row et column major order.]]
Maintenant, supposons que la texture commence à l'adresse <math>A_\text{texture}</math>, qui est l'adresse du premier texel. La texture a une résolution de <math>\text{width}</math> texels de large et <math>\text{height}</math> texels de haut. Par définition, les coordonnées X et Y des texels commencent à 0, ce qui fait que le pixel en haut à gauche a les coordonnées 0,0.
L'adresse du pixel se calcule comme suit :
: <math>A_\text{pixel} = A_\text{texture} + (\text{taille d'une ligne en octets} \times Y) + (\text{taille d'un texel en octets} \times X)</math>
La taille d'un pixel en mémoire est notée T. La taille d'une ligne en mémoire est de <math>width \times T</math>, par définition, vu qu'elle fait <math>width</math> texels. On a donc :
: <math>A_\text{pixel} = A_\text{texture} + (width \times T \times Y) + (T \times X)</math>
La formule se réécrit comme suit :
: <math>A_\text{pixel} = A_\text{texture} + T \times (width \times Y + X)</math>
Le calcul d'adresse est donc assez simple. Malheureusement, les textures ne sont pas stockées de cette manière en mémoire vidéo. En effet, elle se marie mal avec les opérations de filtrage de texture que nous allons voir dans ce qui suit. Le filtrage d'un texel dépend de ses voisins du dessus et du dessous. Le fait que la texture n'est pas forcément parcourue ligne par ligne fait que stocker une texture ligne par ligne n'est pas l'idéal.
De même, les textures sont déformées par la perspective. L'affichage de la texture ne se fait alors pas ligne par ligne, mais en parcourant la texture en diagonale, l'angle de la diagonale correspondant approximativement à l'angle que fait la verticale de la texture avec le regard. Vu qu'on ne connait pas à l'avance l'angle que fera la diagonale de parcours, on doit ruser.
====Les textures tilées====
Une première solution à ce problème est celle des '''textures tilées'''. Avec ces textures, l'image de la texture est découpée en ''tiles'', des rectangles ou en carrés de taille fixe, généralement des carrés de 4 pixels de côté. Les tiles ont une largeur et une longueur égales, afin de simplifier les calculs : on divise X et Y par le même nombre. De plus, leur largeur et leur longueur sont une puissance de deux, afin de simplifier les calculs d'adresse. Les ''tiles'' sont alors mémorisée les unes après les autres dans le fichier de la texture.
[[File:Texture tilée.png|centre|vignette|upright=2|Texture tilée]]
La formule de calcul d'adresse vue plus haut doit être adaptée pour tenir compte des tiles. Pour cela, il faut remplacer la taille d'un texel par la taille d'une tile, et que la largeur de la texture soit exprimée en nombre de tiles. De plus, on doit adapter les coordonnées des texels pour donner des coordonnées de tile. Généralement, les tiles sont des carrés de N pixels de côté, ce qui fait qu'on peut regrouper les lignes et les colonnes par paquets de N. Il suffit donc de diviser Y et X pour obtenir les coordonnées de la tile, de même que la larguer. La formule pour calculer la position de la énième tile est alors la suivante :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + \text{Taille mémoire d'une tile} \times \left( {\text{Width} \over N} \times {Y \over N} + {X \over N} \right)</math>
On peut réécrire le tout comme suit :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + K \times \left( {Y \over N} + X \right)</math>, avec K une constante connue à la compilation des shaders.
Vu que les tiles sont carrées avec une largeur qui est une puissance de deux, la multiplication par la taille d'une tile en mémoire se simplifie : on passe d'une multiplication entière à des décalages de bits. Même chose pour le calcul de l'adresse de la tile à partir des coordonnées x,y : ils impliquent des divisions par une puissance de deux, qui deviennent de simples décalages.
La position d'un pixel dans une tile dépend du format de la texture, mais peut se calculer avec quelques calculs arithmétiques simples. Dans les cas les plus simples, les pixels sont mémorisés ligne par ligne, ou colonne par colonne. Mais ce n'est pas systématiquement le cas. Toujours est-il que les calculs pour déterminer l'adresse sont simples, et ne demandent que quelques additions ou multiplications. Mais avec les formats de texture utilisés actuellement, les tiles sont chargées en entier dans le cache de texture, sans compter que diverses techniques de compression viennent mettre le bazar, comme on le verra dans la suite de cours.
Un avantage de l'organisation en tiles est qu'elle se marie bien avec le parcours des textures. On peut parcourir une texture dans tous les sens, horizontal, vertical, ou diagonal, on sait que les prochains pixels ont de fortes chances d'être dans la même tile. Si on rentre dans une tile par la gauche en haut, on a encore quelques pixels à parcourir dans la tile, par exemple. De même, le filtrage de textures est facilité. On verra dans ce qui va suivre que le filtrage de texture a besoin de lire des blocs de 4 texels, des carrés de 2 pixels de côté. Avec l'organisation en tile, on est certain que les 4 texels seront dans la même tile, sauf s'ils ont le malheur d'être tout au bord d'une tile. Ce dernier cas est assez rare, et il l'est d'autant plus que les tiles sont grandes. Enfin, un dernier avantage est que les tiles sont généralement assez petites pour tenir tout entier dans une ligne de cache. Le cache de texture est donc utilisé à merveille, ce qui rend les accès aux textures plus rapides.
====Les textures basées sur des ''z-order curves''====
Les formats de textures théoriquement optimaux utilisent une '''''Z-order curve''''', illustrée ci-dessous. L'idée est de découper la texture en quatre rectangles identiques, et de stocker ceux-ci les uns à la suite des autres. L'intérieur de ces rectangles est lui aussi découpé en quatre rectangles, et ainsi de suite. Au final, l'ordre des pixels en mémoire est celui illustré ci-dessous.
[[File:Z-CURVE.svg|centre|vignette|upright=2|Construction d'une ''Z-order curve''.]]
Les texels sont stockés les uns à la suite des autres dans la mémoire, en suivant l'ordre donnée par la ''Z-order curve''. Le calcul d'adresse calcule la position du texel en mémoire, par rapport au début de la texture, et ajoute l'adresse du début de la texture. Mais tout le défi est de calculer la position d'un texel en mémoire, à partir des coordonnées x,y. Le calcul peut sembler très compliqué, mais il n'en est rien. Le calcul demande juste de regarder les bits des deux coordonnées et de les combiner d'une manière particulièrement simple. Il suffit de placer le bit de poids fort de la coordonnée x, suivi de celui de la coordonnée y, et de faire ainsi de suite en passant aux bits suivants.
[[File:Zcurve45bits.png|centre|vignette|upright=1.5|Calcul de la position d'un élément dans une ''Z-order curve'' à partir des coordonnées x et y.]]
L'avantage d'une telle organisation est que la textures est découpées en ''tiles'' rectangulaires d'une certaine taille, elles-mêmes découpées en ''tiles'' plus petites, etc. Et il se trouve que cette organisation est parfaite pour le cache de texture. L'idéal pour le cache de texture est de charger une ''tile'' complète dans le cache de textures. Quand on accède à un texel, on s'assure que la ''tile'' complète soit chargée. Mais cela demande de connaitre à l'avance la taille d'une ''tile''. Les formats de texture fournissent généralement une ''tile'' carré de 4 pixels de côté, mais cela donnerait un cache trop petit pour être vraiment utile. Avec cette méthode, on s'assure qu'il y ait une ''tile'' avec la taille optimale. Les ''tiles'' étant découpées en ''tiles'' plus petites, elles-mêmes découpées, et ainsi de suite, on s'assure que la texture est découpées en ''tiles'' de taille variées. Il y aura au moins une ''tile'' qui rentrera tout pile dans le cache.
==Les techniques de rendu à textures multiples==
Nous venons de voir comment une texture est plaquée sur un objet 3D, ou une surface comme un sol. Pour résumer, le calcul de l'adresse d'un texel prend la position du texel par rapport au début de la texture, et ajoute l'adresse du début de la texture. L'adresse mémoire de la texture est connue au moment où le pilote de la carte graphique place la texture dans la mémoire vidéo, et cette information est transmise au matériel par l'intermédiaire du processeur de commande, puis passée aux processeurs de shaders et à l'unité de texture. Le tout est couplé à d'autres informations, la plus importante étant la ''taille de la texture en octets'', pour éviter de déborder lors des accès à la texture.
Néanmoins, il s'agit là du cas le plus simple. Certaines techniques de rendu demandent de choisir la texture à plaquer parmi un ensemble de plusieurs textures. Les techniques en question sont assez variées et n'ont pas grand chose en commun. Les plus connues sont le ''mip-mapping'', le ''cube-mapping'' et les textures virtuelles. Le ''mip-mapping'' sert à filtrer les textures, chose qu'on expliquera plus tard, le ''cube-mapping'' sert à simuler des réflexions sur un objet en plaquant une texture de l'environnement dessus, les textures virtuelles sont une optimisation pour les textures des terrains de grande taille. Mais malgré leurs différences, elles demandent de choisir quelle texture plaquer entre plusieurs textures de base. En clair, l'adresse de base de la texture varie selon la situation. Voyons-les dans le détail.
===Le mip-mapping===
Le '''mip-mapping''' a pour but de légèrement améliorer les graphismes des objets lointains, tout en rendant les calculs de texture plus rapides. Formellement, le ''mip-mapping'' est une technique de filtrage de texture, mais nous l'abordons maintenant car elle est surtout liée au calcul d'adresse. Les unités de texture ont des circuits de filtrage de texture séparés des circuits de ''mip-mapping'' et de calcul d'adresse, d'où le fait que nous en parlons séparément.
Le problème résolu par le ''mip-mapping'' est le rendu des textures lointaines. Si une texture est plaquée sur un objet lointain, une bonne partie des détails est invisible pour l'utilisateur. Un pixel de l'écran est associé à plusieurs texels. Idéalement, la carte graphiques devrait lire tous ces texels et en faire une sorte de moyenne pondérée, pour calculer la couleur finale du pixel. Mais dans les faits, ce serait très gourmand et compliqué à implémenter en hardware. Une solution serait de ne garder que quelque texels, mais cela a tendance à créer des artefacts visuels (les textures affichées ont tendance à pixeliser). Le ''mip-mapping'' permet de réduire ces deux problèmes en même temps en précalculant cette moyenne pondérée pour des distances prédéfinies.
L'idée est d'utiliser plusieurs exemplaires d'une même texture à des résolutions différentes, chaque exemplaire étant adapté à une certaine distance. Par exemple, une texture sera stocké avec un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre de 128 * 128 et ainsi de suite jusqu’à un dernier exemplaire de 32 * 32 pixel. Chaque exemplaire correspond à un '''niveau de détail''', aussi appelé ''Level Of Detail'' (abrévié en LOD). La résolution utilisée diminue d'autant plus que l'objet est situé loin de la caméra. Les objets proches seront rendus avec la texture 512*512, ceux plus lointains seront rendus avec la texture de résolution 256*256, les textures 128*128 seront utilisées encore plus loin, et ainsi de suite jusqu'aux objets les plus lointains qui sont rendus avec la texture la plus petite de 32*32.
[[File:MipMap Example STS101.jpg|centre|vignette|upright=2|Exemples de mip-maps.]]
Le ''mip-mapping'' améliore grandement la qualité d'image. L'image d'exemple ci-dessous le montre assez bien.
[[File:Mipmapping example.png|centre|vignette|upright=2|Exemple de mipmapping.]]
Pour faciliter les calculs d'adresse, les LOD d'une même texture sont stockées les uns après les autres en mémoire (dans un tableau, comme diraient les programmeurs). Ainsi, pas besoin de se souvenir de la position en mémoire de chaque LOD : l'adresse de la texture de base, et quelques astuces arithmétiques suffisent. Prenons le cas où la texture de base a une taille L. le premier exemplaire est à l'adresse 0, le second niveau de détail est à l'adresse L, le troisième à l'adresse L + L/4, le suivant à l'adresse L + L/4 + L/16, et ainsi de suite. Le calcul d'adresse demande juste connaître le niveau de détails souhaité et l'adresse de base de la texture. Le niveau de détail voulu est calculé par les pixel shaders, en fonction de la coordonnée de profondeur du pixel à traiter.
Évidemment, cette technique consomme de la mémoire vidéo, vu que chaque texture est dupliquée en plusieurs exemplaires, en plusieurs LOD. Dans le détail, la technique du mip-mapping prend au maximum 33% de mémoire en plus (sans compression). Cela vient du fait qu'en prenant une texture dexu fois plus petite, elle prend 4 fois moins de mémoire : 2 fois moins de pixels en largeur, et 2 fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des LODs, texture de base comprise, prendra X + (X/4) + (X/16) + (X/256) + … Un petit calcul de limite donne 4/3 * X, soit 33% de plus.
===Le cube-mapping===
[[File:Cube mapped reflection example 2.JPG|vignette|Exemple de reflets environnementaux.]]
L''''environnement-mapping''' est une technique de calcul de divers effets graphiques liés à l'environnement, notamment des réflexions. L'idée est de plaquer une texture pré-calculée pour simuler l'effet de l'environnement sur une surface ou un objet 3D. Il en existe plusieurs versions différentes, mais la seule utilisée de nos jours est le ''cube-mapping'', où la texture de l'environnement est plaquée sur un cube, d'où son nom. Le cube en question est utilisé différemment suivant ce que l'on cherche à faire avec le ''cube-mapping''. Les deux utilisations principales sont le rendu du ciel et des décors, et les réflexions sur la surface des objets. Dans les deux cas, l'idée est de précalculer ce que l'on voit du point de vue de la caméra. On place la caméra dans la scène 3D, on place un cube centré sur la caméra, le cube est texturé avec ce que l'on voit de l'environnement depuis la caméra/l'objet de son point de vue.
[[File:Panorama cube map.png|centre|vignette|upright=2|L'illustration montre en premier lieu une ''cubemap'' avec les six faces mises en évidence, puis quel environnement 3D elle permet de simuler, le troisième illustration montrant comment la ''cubemap'' est utilisée pour simuler l'environnement.]]
Le rendu du ciel et des décors lointains dans les jeux vidéo se base sur des '''''skybox''''', à savoir un cube centré sur la caméra, sur lequel on ajoute des textures de ciel ou de décors lointains. Le cube est recouvert par une texture, qui correspond à ce que l'on voit quand on dirige le regard de la caméra vers cette face. Contrairement à ce qu'on pourrait croire, la skybox n'est pas les limites de la scène 3D, les limites du niveau d'un jeu vidéo ou quoique ce soit d'autre de lié à la physique de la scène 3D. La skybox est centrée sur la caméra, elle suit la caméra dans son mouvement. Centrer la skybox sur la caméra permet de simuler des décors très lointains, suffisamment lointain pour qu'on n'ait pas l'illusion de s'en rapprocher en se déplaçant dans la map. De plus, cela évite d'avoir à faire trop de calculs à chaque fois que l'on bouge la caméra. La texture plaquée sur le cube est une texture unique, elle-même découpée en six sous-textures, une par face du cube.
[[File:Skybox example.png|centre|vignette|upright=2|Exemple de Skybox.]]
[[File:Cube mapped reflection example.jpg|vignette|Réflexions calculées par une ''cubemap''.]]
Le ''cube-mapping'' est aussi utilisé pour des reflets. L'idée est de simuler les reflets en plaquant une texture pré-calculée sur l'objet réflecteur. La texture pré-calculée est un dessin de l'environnement qui se reflète sur l'objet, un dessin du reflet à afficher. En la plaquant la texture sur l'objet, on simule ainsi des reflets de l'environnement, mais on ne peut pas calculer d'autres reflets comme les reflets objets mobiles comme les personnages. Et il se trouve que la texture pré-calculée est une ''cubemap''. Pour les environnements ouverts, c'est la ''skybox'' qui est utilisée, ce qui permet de simuler les reflets dans les flaques d'eau ou dans des lacs/océans/autres. Pour les environnements intérieurs, c'est une cubemap spécifique qui utilisée. Par exemple, pour l'intérieur d'une maison, on a une ''cubemap'' par pièce de la maison. Les reflets se calculent en précisant quelle ''cubemap'' appliquer sur l'objet en fonction de la direction du regard.
[[File:Cube map level.png|centre|vignette|Cube map de l'intérieur d'une pièce d'un niveau de jeux vidéo.]]
Toujours est-il que les textures utilisées pour le ''cubemmapping'', appelées des ''cubemaps'', sont en réalité la concaténation de six textures différentes. En mémoire vidéo, la ''cubemap'' est stockée comme six textures les unes à la suite des autres. Lors du rendu, on doit préciser quelle face du cube utiliser, ce qui fait 6 possibilités. On a le même problème qu'avec les niveaux de détail, sauf que ce sont les faces d'une ''cubemap'' qui remplacent les textures de niveaux de détails. L'accès en mémoire doit donc préciser quelle portion de la ''cubemap'' il faut accéder. Et l'accès mémoire se complexifie donc. Surtout que l'accès en question varie beaucoup suivant l'API graphique utilisée, et donc suivant la carte graphique.
Les API 3D assez anciennes ne gérent pas nativement les ''cubemaps'', qui doivent être émulées en logiciel en utilisant six textures différentes. Le pixel shader décide donc quelle ''cubemap'' utiliser, avec quelques calculs sur la direction du regard. L'accès se fait d'une manière assez simple : le shader choisit quelle texture utiliser. Les API 3D récentes gèrent nativement les ''cubemaps''. Dans le cas le plus simple,pour les versions les plus vielles de ces API, les six faces sont numérotées et l'accès à une ''cubemap'' précise quel face utiliser en donnant son numéro. La carte graphique choisit alors automatiquement la bonne texture, mais cela demande de laisser le calcul de la bonne face au pixel shader. D'autres API 3D et cartes graphiques font autrement. Dans les API 3D modenres, les ''cubemap'' sont gérées comme des textures en trois dimensions, adressées avec trois coordonnées u,v,w. La carte graphique utilise ces trois coordonnées de manière à en déduire quelle est la face pertinente, mais aussi les coordonnées u,v dans la texture de la face.
==L'implémentation matérielle du placage de textures==
Pour résumer, la lecture d'un texel demande d'effectuer plusieurs étapes. Dans le cas le plus simple, sans ''mip-mapping'' ou ''cubemapping'', on doit effectuer les étapes suivantes :
* Il faut d'abord normaliser les coordonnées de texture pour qu'elles tombent dans l'intervalle [0,1] en fonction du mode d'adressage désiré.
* Ensuite, les coordonnées u,v doivent être converties en coordonnées entières, ce qui demande une multiplication flottante.
* Enfin, l'adresse finale est calculée à partir des coordonnées entières et en ajoutant l'adresse de base de la texture (et éventuellement avec d'autres calculs arithmétiques suivant le format de la texture).
Tout cela pourrait être fait par le pixel shaders, mais cela implique beaucoup de calculs répétitifs et d'opérations arithmétiques assez lourdes, avec des multiplications flottantes, des additions et des multiplications entières, etc. Faire faire tous ces calculs par les shaders serait couteux en performance, sans compter que les shaders deviendraient plus gros et que cela aurait des conséquences sur le cache d'instruction. De plus, certaines de ces étapes peuvent se faire en parallèle, comme les deux premières, ce qui colle mal avec l'aspect sériel des shaders.
Aussi, les processeurs de shaders incorporent une unité de calcul d'adresse spéciale pour faire ces calculs directement en matériel. L'unité de texture contient au minimum deux circuits : un circuit de calcul d'adresse, et un circuit d'accès à la mémoire. Toute la difficulté tient dans le calcul d'adresse, plus que dans le circuit de lecture. Le calcul d'adresse est conceptuellement réalisé en deux étapes. La première étape qui transforme les coordonnées u,v en coordonnées x,y qui donne le numéro de la ligne et de la colonne du texel dans la texture. La seconde étape prend ces deux coordonnées x,y, l'adresse de la texture, et détermine l'adresse de la tile à lire.
[[File:Unité de texture simple.png|centre|vignette|upright=2|Unité de texture simple]]
===L'implémentation du mip-mapping===
Le ''mip-mapping'' est lui aussi pris en charge par l'unité de calcul d'adresse, car cette technique change l'adresse de base de la texture. La gestion du ''mip-mapping'' est cependant assez complexe. Il est possible de laisser le pixel shader calculer quel niveau de détail utiliser, en fonction de la coordonnée de profondeur z du pixel à afficher. La carte graphique détermine alors automatiquement quelle texture lire, quel niveau de détail, automatiquement. Elle détermine aussi la bonne résolution pour la texture, qui est égal à la résolution de la texture de base, divisée par le niveau de détail. Pour résumer, le niveau de détail est envoyé aux unités de texture, qui s'occupent de calculer l'adresse de base et la résolution adéquates. Quelques calculs arithmétiques simples, donc, qui s'implémentent facilement avec quelques circuits.
Mais une autre méthode laisse la carte graphique déterminer le niveau de détail par elle-même. Dans ce cas, cela demande, outre les deux coordonnées de texture, de calculer la dérivée de ces deux coordonnées dans le sens horizontal et vertical, ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Les quatre dérivées sont les suivantes :
: <math>\frac{du}{dx}</math>, <math>\frac{dv}{dx}</math>, <math>\frac{du}{dy}</math>, <math>\frac{dv}{dy}</math>
Un bon moyen pour obtenir les dérivées demande de regrouper les pixels par groupes de 4 et de faire la différence entre leurs coordonnées de texture respectives. On peut calculer les deux dérivées horizontales en comparant les deux pixels sur la même ligne, et les deux dérivées verticales en comparant les deux pixels sur la même colonne. Mais cela demande de rastériser les pixels par groupes de 4, par ''quads''. Et c'est ce qui est fait sur les cartes graphiques actuelles, qui rastérisent des groupes de 4 pixels à la fois.
[[File:Texture sampler unit with mipmapping.png|centre|vignette|upright=2.0|Unité de texture avec mipmapping.]]
Malheureusement, le calcul exact utilisé pour le choix de la mip-map dépend du GPU considéré et peu de chose est connu quant à ces algorithmes. Il est possible d'inférer le comportement à partir d'observations, mais guère plus. Pour ceux qui veulent en savoir plus, je conseille la lecture de cet article de blog :
* [https://pema.dev/2025/05/09/mipmaps-too-much-detail/ Mipmap selection in too much detail]
===La gestion des accès mémoire===
Enfin, l'unité de texture doit tenir compte du fait que la mémoire vidéo met du temps à lire une texture. En théorie, l'unité de texture ne devrait pas accepter de nouvelle demande de lecture tant que celle en cours n'est pas terminée. Mais faire ainsi demanderait de bloquer tout le pipeline, de l'''input assembler'' au unités de''shaders'', ce qui est tout sauf pratique et nuirait grandement aux performances.
Une solution alternative consiste à mettre en attente les demandes de lectures de texture pendant que la mémoire est occupée. La manière la plus simple d'implémenter des accès mémoire multiples est de les mettre en attente dans une petite mémoire FIFO. Cela implique que les accès mémoire s’exécutent dans l'ordre demandé par le ''shader'' et/ou l'unité de rastérisation, il n'y a pas de réorganisation des accès mémoire ou d’exécution dans le désordre des accès mémoire.
[[File:Texture prefetching.png|centre|vignette|upright=1.5|Accès mémoire simultanés.]]
Évidemment, quand la mémoire FIFO est pleine, le pipeline est alors totalement bloqué. Le rasteriser est prévenu que l'unité de texture ne peut pas accepter de nouvelle lecture de texture. En pratique, la FIFO est généralement d'une taille respectable et permet de mettre en attente beaucoup de demandes de lecture de texture. Il faut de plus noter qu'il y a une FIFO par processeur de ''shader'' sur les cartes graphiques modernes. Quand elle est pleine, le processeur cesse d'exécuter de nouveaux accès mémoire, mais peut continuer à exécuter des ''shaders'' dans les autres unités de calcul, pas besoin de bloquer complétement le pipeline.
===L'intégration du cache de textures===
Il faut noter que les unités de texture incorporent aussi un cache de texture, voire plusieurs. L'intégration des caches de texture avec la mémoire FIFO précédente est quelque peu compliqué, car il faut garantir que les lectures de texture se fassent dans le bon ordre. On ne peut pas exécuter une lecture dans le cache alors que des lectures précédentes sont en attente de lecture en mémoire vidéo. Et cela pose un gros problème : une lecture dans le cache de texture prend quelques dizaines de cycles d'horloge, alors qu'une lecture en mémoire vidéo en prend facilement 400 à 800 cycles, parfois plus. Et cela fait que l'ordre des accès mémoire peut s'inverser.
Prenons par exemple un accès au cache précédé et suivi par deux accès en mémoire vidéo. Le premier démarre au cycle 1, et se termine au cycle numéro 400. L'accès au cache commence au cycle 2 et se termine 20 cycles après, au cycle numéro 22. En clair, la lecture dans le cache s'est terminée avant l'accès mémoire qui le précède. Les textures ne sont donc plus lues dans l'ordre. Et il faut trouver une solution pour éviter cela.
La solution est de retarder les lectures dans le cache tant que tous les accès précédents ne sont pas terminés. Mais pour retarder les lectures en question, il faut d'abord savoir si la lecture atterrit dans le cache ou non, ce qui demande d'accéder au cache. On fait face à un dilemme : on veut retarder les accès au cache, mais les différencier des lectures déclenchant des accès mémoire demande d'accéder au cache en premier lieu. La solution est décrite dans l'article "Prefetching in a Texture Cache Architecture" par Igehy et ses collègues. Elle se base sur deux idées combinées ensemble.
La première idée est de séparer l'accès au cache en deux : une étape qui vérifie si les texels à lire sont dans le cache, et une étape qui accède aux données dans le cache lui-même. Un cache de texture est donc composé de deux circuits principaux. Le premier vérifie la présence des texels dans le cache. Il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Ensuite, en plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
La seconde idée est de retarder l'accès au cache entre les deux phases. La première étape d'un accès mémoire vérifie si la donnée est dans le cache ou non. Puis, on retarde la lecture des données, pour attendre que toutes les lectures précédentes soient terminées. Et enfin, troisième étape : la lecture des texels dans la mémoire cache proprement dite. Les accès mémoire passant par la mémoire vidéo se font de la même manière, à une différence près : la lecture dans le cache est remplacée par la lecture en mémoire vidéo. Tout démarre avec une demande à l'unité de tags, qui vérifie si le texel est dans le cache ou non. Puis on retarde l'accès tant que la mémoire vidéo est occupée, puis on effectue la lecture en mémoire vidéo.
Si ce n'est pas le cas, l'accès mémoire est envoyé à la mémoire vidéo comme précédemment, à savoir qu'il est mis en attente dans une mémoire FIFO, puis envoyé à la mémoire vidéo dès que celle-ci est libre. Mais en sortie de la mémoire, la donnée lue est envoyée dans le cache de texture, par dans l'unité de filtrage. Pour savoir où placer la donnée lue, l'unité de tag a réservé une ligne de cache précise, une adresse bien précise. L'adresse en question est disponible en lisant une autre mémoire FIFO, qui a mis en attente l'adresse en question, en attendant que l'accès mémoire se termine. La donnée est alors écrite dans le cache, puis lue par l'unité de filtrage de textures.
Pour une lecture dans le cache, le déroulement est similaire, mais sans le passage par la mémoire. La lecture fait une demande à l'unité de tag, et celle-ci répond que la donnée est bien dans le cache. Elle place alors l'adresse à lire dans la file d'attente. Une fois que les accès mémoire précédents sont terminés, l'adresse sort de la file d'attente et est envoyée à la mémoire de données. La lecture s'effectue, les texels sont envoyés à l'unité de filtrage de textures. La seule différence avec un ''phased cache'' normal est l'insertion de l'adresse à lire dans une FIFO qui vise à mettre en attente
[[File:Unité de texture avec un cache de texture.png|centre|vignette|upright=2.0|Unité de texture avec un cache de texture]]
Pour résumer, l'implémentation précédente garantit une exécution des lectures dans leur ordre d'arrivée. Et pour cela, elle retarde les lectures dans le cache tant que les lectures en mémoire précédentes ne sont pas terminées. L'accès au cache est plus rapide que l'accès en mémoire vidéo, mais le retard ajouté pour garantir l'ordre des lectures fait que le temps d'accès est très long.
==Le filtrage de textures==
Plaquer des textures sans autre forme de procès ne suffit pas à garantir des graphismes d'une qualité époustouflante. La raison est que les sommets et les texels ne tombent pas tout pile sur un pixel de l'écran : le sommet associé au texel peut être un petit peu trop en haut, ou trop à gauche, etc. Une explication plus concrète fait intervenir les coordonnées de texture. Souvenez-vous que lorsque l'on traduit une coordonnée de texture u,v en coordonnées x,y, on obtient un résultat qui ne tombe pas forcément juste. Souvent, le résultat a une partie fractionnaire. Si celle-ci est non-nulle, cela signifie que le texel/sommet n'est pas situé exactement sur le pixel voulu et que celui-ci est situé à une certaine distance. Concrètement, le pixel tombe entre quatre texels, comme indiqué ci-dessous.
[[File:Filtrage texture.png|centre|vignette|upright=2.0|Position du pixel par rapport aux texels.]]
Pour résoudre ce problème, on doit utiliser différentes techniques d'interpolation, aussi appelées techniques de '''filtrage de texture''', qui visent à calculer la couleur du pixel final en fonction des texels qui l'entourent. Il existe de nombreux types de filtrage de textures, qu'il s'agisse du filtrage linéaire, bilinéaire, trilinéaire, anisotropique et bien d'autres.
Tous ont besoin d'avoir certaines informations qui sont généralement fournies par les circuits de calcul d'adresse. La première est clairement la partie fractionnaire des coordonnées x,y. La seconde est la dérivée de ces deux coordonnées dans le sens horizontal et vertical., ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Toujours est-il que le filtrage de texture est une opération assez lourde, qui demande beaucoup de calculs arithmétiques. On pourrait en théorie le faire dans les pixels shaders, mais le cout en performance serait absolument insoutenable. Aussi, les cartes graphiques intègrent toutes un circuit dédié au filtrage de texture, le ''texture sampler''. Même les plus anciennes cartes graphiques incorporent une unité de filtrage de texture, ce qui nous montre à quel point cette opération est importante.
[[File:Texture unit.png|centre|vignette|upright=2.0|Unité de texture.]]
On peut configurer la carte graphique de manière à ce qu'elle fasse soit du filtrage bilinéaire, soit du filtrage trilinéaire, on peut configurer le niveau de filtrage anisotropique, etc. Cela peut se faire dans les options de la carte graphique, mais cela peut aussi être géré par l'application. La majorité des jeux vidéos permettent de régler cela dans les options. Ces réglages ne concernent pas la texture elle-même, mais plutôt la manière dont l'unité de texture doit fonctionner. Ces réglages sur l''''état de l'unité de texture''' sont mémorisés quelque part, soit dans l'unité de texture elle-même, soit fournies avec la ressource de texture elle-même, tout dépend de la carte graphique. Certaines cartes graphiques mémorisent ces réglages dans les unités de texture ou dans le processeur de commande, et tout changement demande alors de réinitialiser l'état des unités de texture, ce qui prend un peu de temps. D'autres placent ces réglages dans les ressources de texture elles-mêmes, ce qui rend les modifications de configuration plus rapides, mais demande plus de circuits. D'autres cartes graphiques mélangent les deux options, certains réglages étant globaux, d'autres transmis avec la texture. Bref, difficile de faire des généralités, tout dépend du matériel et le pilote de la carte graphique cache tout cela sous le tapis.
Maintenant que cela est dit, voyons quelles sont les différentes méthodes de filtrage de texture et comment la carte graphique fait pour les calculer.
===Le filtrage au plus proche===
La méthode de filtrage la plus simple consiste à colorier avec le texel le plus proche. Cela revient tout simplement à ne pas tenir compte de la partie fractionnaire des coordonnées x,y, ce qui est très simple à implémenter en matériel. C'est ce que l'on appelle le '''filtrage au plus proche''', aussi appelé ''nearest filtering''.
Autant être franc, le résultat est assez pixelisé et peu agréable à l’œil. Par contre, le résultat est très rapide à calculer, vu qu'il ne demande aucun calcul à proprement parler. Elle ne fait pas appel à la parti fractionnaire des coordonnées entières de texture, ni aux dérivées de ces coordonnées. On peut combiner cette technique avec le mip-mapping, ce qui donne un résultat bien meilleur, bien que loin d'être satisfaisant. Au passage, toutes les techniques de filtrage de texture peuvent se combiner avec du mip-mapping, certaines ne pouvant pas faire sans.
[[File:Interpolation-nearest.svg|centre|vignette|Filtrage de texture au plus proche.]]
===Le filtrage linéaire===
Le filtrage le plus simple est le '''filtrage linéaire'''. Il effectue une interpolation linéaire entre deux mip-maps, deux niveaux de détails. Pour comprendre l'idée, nous allons prendre une situation très simple, avec une texture carrée de 512 texels de côté. Le mip-mapping crée plusieurs textures : une de 256 texels de côté, une de 128 texels, une de 64, etc. Maintenant, la texture est sur un objet à une certaine distance de l'écran, vu de face. Le résultat est qu'elle correspond à l'écran à un carré de 300 pixels de côté (pas d'erreur : pixels, pas texels). Dans ce cas, la texture se trouve entre deux mip-maps : celle de 512 pixels de côté, celle de 256. Laquelle choisir ? Le filtrage au plus proche prend la texture de 512 pixels de côté. Le filtrage linéaire lui, fait autrement.
Vu que la texture est entre deux mip-maps, l'idée est de prendre le texel au plus proche dans chaque texture et de faire une sorte de moyenne appelée l'interpolation linéaire. L'interpolation par du principe que la couleur varie entre les deux texels en suivant une fonction affine, illustrée ci-dessous. Ce ne serait évidemment pas le cas dans le monde réel, mais on supposer cela donne une bonne approximation de ce à quoi ressemblerait une texture à plus haute résolution. On peut alors calculer la couleur du pixel par une simple moyenne pondérée par la distance. Le résultat est que les transitions entre deux niveaux de détails sont plus lisses, moins abruptes.
[[File:Lin interp -é.png|centre|vignette|upright=2.0|Interpolation linéaire.]]
===Le filtrage bilinéaire===
Le filtrage bilinéaire effectue une sorte de moyenne pondérée des quatre texels les plus proches du pixel à afficher. Pour cela, rappelez-vous ce qui a été dit plus haut : les coordonnées x,y d'un pixel ont une partie entière et une partie fractionnaire. Le filtrage au plus proche élimine les parties fractionnaires, ce qui donne une coordonnée x,y. Avec le filtrage bilinéaire, on prend les texels de coordonnées (x,y) ; (x+1,y) ; (x,y+1) ; (x+1,y+1), le pixel étant entre ces 4 texels.
Mais le filtrage ne fait pas qu'une simple moyenne, il prend en compte les parties fractionnaires pour faire la moyenne. En effet, le pixel n'est pas au milieu du carré de texel, il est quelque part mais est souvent plus proche d'un texel que des autres. Et il faut donc pondérer la moyenne par les distances aux 4 texels. Pour cela, la moyenne est calculée à partir d'interpolations linéaires. Avec 4 pixels, nous allons devoir calculer la couleur de deux points intermédiaires. La couleur de ces deux points se calcule par interpolation linéaire, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.
[[File:Bilin3.png|centre|vignette|upright=2|Filtrage bilinéaire de texture.]]
Le circuit qui permet de faire l'interpolation bilinéaire est particulièrement simple. On trouve un circuit de chaque pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu, et un pour la transparence. Chacun de ces circuit est composé de sous-circuits chargés d'effectuer une interpolation linéaire, reliés comme suit.
[[File:Texture sampler unit.png|centre|vignette|Unité de filtrage bilinéaire.]]
Vous noterez que le filtrage bilinéaire accède à 4 pixels en même temps. Fort heureusement, les textures sont stockées de manière à ce qu'on puisse charger les 4 pixels en une fois, comme on l'a vu plus haut. Le filtrage bilinéaire a de fortes chances que les 4 pixels filtrés soient dans la même ''tile'', la seule exception étant quand ils sont tout juste sur le bord d'une ''tile''.
: La console de jeu Nintendo 64 n'utilise que trois pixels au lieu de quatre dans son interpolation bilinéaire, qui en devient une interpolation quasi-bilinéaire. La raison derrière ce choix est une question de performances, comme beaucoup de décisions de ce genre. Le résultat est un rendu imparfait de certaines textures.
===Le filtrage trilinéaire===
Avec le filtrage bilinéaire, des discontinuités apparaissent sur certaines surfaces. Par exemple, pensez à une texture de sol : elle est appliquée plusieurs fois sur toute la surface du sol. A une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512*512 à 256*256, ce qui est visible pour un joueur attentif. De telles transitions sont lissées grâce au filtrage linéaire, il n'y a plus qu'à le combiner avec le filtrage bilinéaire. Rien d’incompatible : le premier filtre l'intérieur d'une mip-map, le second combine deux mip-maps.
Le filtrage trilinéaire prend les deux mip-maps les plus proches, fait un filtrage bilinéaire avec chacune, puis fait une « une moyenne » pondérée entre les deux résultats. Le circuit de filtrage trilinéaire existe en plusieurs versions. La plus simple, illustrée ci-dessous, effectue deux filtrages bilinéaires en parallèle, dans deux circuits séparés, puis combine leurs résultats avec un circuit d'interpolation linéaire. Mais ce circuit nécessite de charger 8 texels simultanément. Qui plus est, ces 8 texels ne sont pas consécutifs en mémoire, car ils sont dans deux niveaux de détails/mip-maps différents.
[[File:Parallel trilinear filtering.png|centre|vignette|upright=2.0|Unité de filtrage trilinéaire parallèle.]]
Vu qu'on lit des texels dans deux mip-maps, les texels sont lus en deux fois : 4 texels provenant de la première mip-map, suivis par les 4 texels de l'autre mip-map. Les 4 premiers texels doivent donc être mis en attente dans des registres, en attendant que les 4 autres arrivent. Une amélioration du circuit précédent gère cela en ajoutant des registres. Il lit les 4 premiers texels, les filtre avec une interpolation bilinéaire, et mémorise le résultat dans un registre. Puis, il lit les 4 autres texels, les filtre, et met le résultat dans un second registre. A ce moment là, un circuit d'interpolation linéaire finit le travail. On économise donc un circuit d'interpolation bilinéaire, sans que les performances soient trop impactées.
[[File:Filtrage trilineaire.png|centre|vignette|upright=1.0|Unité de filtrage trilineaire série.]]
Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le temps où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. À chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients, et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.
===Le filtrage anisotrope===
D'autres artefacts peuvent survenir lors de l'application d'une texture, la perspective pouvant déformer les textures et entraîner l'apparition de flou. La raison à cela est que les techniques de filtrage de texture précédentes partent du principe que la texture est vue de face. Prenez une texture carrée, par exemple. Vue de face, elle ressemble à un carré sur l'écran. Mais tournez la caméra, de manière à voir la texture de biais, avec un angle, et vous verrez que la forme de la texture sur l'écran est un trapèze, pas un carré. Cette déformation liée à la perspective n'est pas prise en compte par les méthodes de filtrage de texture précédentes. Pour le dire autrement, les techniques de filtrage précédentes partent du principe que les 4 texels qui entourent un pixel forment un carré, ce qui est vrai si la texture est vue de face, sans angle, mais ne l'est pas si la texture n'est pas perpendiculaire à l'axe de la caméra. Du point de vue de la caméra, les 4 texels forment un trapèze d'autant moins proche d'un carré que l'angle est grand.
Pour corriger cela, les chercheurs ont inventé le '''filtrage anisotrope'''. En fait, je devrais plutôt dire : LES filtrages anisotropes. Il en existe un grand nombre, dont certains ne sont pas utilisés dans les cartes graphiques actuelles, soit car ils trop gourmand en accès mémoires et en calculs pour être efficaces, soit car ils ne sont pas pratiques à mettre en œuvre. Il est très difficile de savoir quelles sont les techniques de filtrage de texture utilisées par les cartes graphiques, qu'elles soient récentes ou anciennes. Beaucoup de ces technologies sont brevetées ou gardées secrètes, et il faudrait vraiment creuser les brevets déposés par les fabricants de GPU pour en savoir plus. Les algorithmes en question seraient de plus difficiles à comprendre, les méthodes mathématiques cachées derrière ces méthodes de filtrage n'étant pas des plus simple.
[[File:Anisotropic filtering en.png|centre|vignette|upright=2|Exemple de filtrage anisotrope.]]
==La compression de textures==
Les textures les plus grosses peuvent aller jusqu'au mébioctet, ce qui est beaucoup. Pour limiter la casse, les textures sont compressées. La '''compression de texture''' réduit la taille des textures, ce qui peut se faire avec ou sans perte de qualité. Elle entraîne souvent une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande. Mais il s'agit là d'une technique très simple, beaucoup plus simple que les techniques que nous allons voir dans cette section. Nous allons voir quelque algorithmes de compression de textures de complexité intermédiaire, mais n'allons pas voir l'état de l'art. Il existe des formats de texture plus récents que ceux qui nous allons aborder, comme l{{'}}''Ericsson Texture Compression'' ou l{{'}}''Adaptive Scalable Texture Compression'', plus complexes et plus efficaces.
Notons que les textures sont compressées dans les fichiers du jeu, mais aussi en mémoire vidéo. Les textures sont décompressées lors de la lecture. Pour cela, la carte graphique contient alors un circuit, capable de décompresser les textures lorsqu'on les lit en mémoire vidéo. Les cartes graphiques supportent un grand nombre de formats de textures, au niveau du circuit de décompression. Du fait que les textures sont décompressées à la volée, les techniques de compression utilisées sont assez particulières. La carte graphique ne peut pas décompresser une texture entière avant de pouvoir l'utiliser dans un ''pixel shader''. A la place, on doit pouvoir lire un morceau de texture, et le décompresser à la volée. On ne peut utiliser les méthodes de compression du JPEG, ou d'autres formats de compression d'image. Ces dernières ne permettent pas de décompresser une image morceau par morceau.
Pour permettre une décompression/compression à la volée, les textures sont des textures tilées, généralement découpées en tiles de 4 * 4 texels. Les ''tiles'' sont compressées indépendamment les unes des autres. Et surtout, avec ou sans compression, la position des tiles en mémoire ne change pas. On trouve toujours une tile tous les T octets, peu importe que la tile soit compressée ou non. Par contre, une tile compressée n'occupera pas T octets, mais moins, là où une tile compressée occupera la totalité des T octets. En clair, compresser une tile fait qu'il y a des vides entre deux tiles dans al mémoire vidéo, mais ne change rien à leur place en mémoire vidéo qui est prédéterminée, peu importe que la texture soit compressée ou non. L'intérêt de la compression de textures n'est pas de réduire la taille de la texture en mémoire vidéo, mais de réduire la quantité de données à lire/écrire en mémoire vidéo. Au lieu de lire T octets pour une tile non-compressée, on pourra en lire moins.
===La palette indicée et la technique de ''Vector quantization''===
La technique de compression des textures la plus simple est celle de la '''palette indicée''', que l'on a entraperçue dans le chapitre sur les cartes d'affichage. La technique de '''''vector quantization''''' peut être vue comme une amélioration de la palette, qui travaille non pas sur des texels, mais sur des ''tiles''. À l'intérieur de la carte graphique, on trouve une table qui stocke toutes les ''tiles'' possibles. Chaque ''tile'' se voit attribuer un numéro, et la texture sera composé d'une suite de ces numéros. Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l’embarqué utilisent ce genre de compression.
===Les algorithmes de ''Block Truncation coding''===
La première technique de compression élaborée est celle du '''''Block Truncation Coding''''', qui ne marche que pour les images en niveaux de gris. Le BTC ne mémorise que deux niveaux de gris par ''tile'', que nous appellerons couleur 1 et couleur 2, les deux niveaux de gris n'étant pas le même d'une ''tile'' à l'autre. Chaque pixel d'une ''tile'' est obligatoirement colorié avec un de ces niveaux de gris. Pour chaque pixel d'une ''tile'', on mémorise sa couleur avec un bit : 0 pour couleur 1, et 1 pour couleur 2. Chaque ''tile'' est donc codée par deux entiers, qui codent chacun un niveau de gris, et une suite de bits pour les pixels proprement dit. Le circuit de décompression est alors vraiment très simple, comme illustré ci-dessous.
[[File:Block Truncation coding.jpg|centre|vignette|upright=2.0|Block Truncation coding.]]
La technique du BTC peut être appliquée non pas du des niveaux de gris, mais pour chaque composante Rouge, Vert et Bleu. Dans ces conditions, chaque ''tile'' est séparée en trois sous-''tiles'' : un sous-bloc pour la composante verte, un autre pour le rouge, et un dernier pour le bleu. Cela prend donc trois fois plus de place en mémoire que le BTC pur, mais cela permet de gérer les images couleur.
===Le format de compression S3TC / DXTC===
L'algorithme de '''Color Cell Compression''', ou CCC, améliore le BTC pour qu'il gère des couleurs autre que des niveaux de gris. Ce CCC remplace les deux niveaux de gris par deux couleurs. Une ''tile'' est donc codée avec un entier 32 bits par couleur, et une suite de bits pour les pixels. Le circuit de décompression est identique à celui utilisé pour le BTC.
[[File:Color Cell Compression.jpg|centre|vignette|Color Cell Compression.]]
[[File:Dxt1-memory-layout.png|vignette|Dxt1 et ''color cell compression''.]]
Le format de compression de texture utilisé de base par Direct X, le DXTC, est une version amliorée de l'algorithme précédent. Il est décliné en plusieurs versions : DXTC1, DXTC2, etc. La première version du DXTC est une sorte d'amélioration du CCC : il ajoute une gestion minimale de transparence, et découpe la texture à compresser en ''tiles'' de 4 pixels de côté. La différence, c'est que la couleur finale d'un texel est un mélange des deux couleurs attribuée au bloc. Pour indiquer comment faire ce mélange, on trouve deux bits de contrôle par texel.
Si jamais la couleur 1 < couleur2, ces deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (2 * Couleur1 + Couleur2) / 3
* 11 = (Couleur1 + 2 * Couleur2) / 3
Sinon, les deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (Couleur1 + Couleur2) / 2
* 11 = Transparent
[[File:DXTC.jpg|centre|vignette|DXTC.]]
Le circuit de décompression du DXTC ressemble alors à ceci :
[[File:Circuit de décompression du DXTC.jpg|centre|vignette|upright=2.0|Circuit de décompression du DXTC.]]
===Les format DXTC 2, 3, 4 et 5 : l'ajout de la transparence===
Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3, lui-même replacé par le DXT4 et par le DXT5. Dans le DXT3, la transparence fait son apparition. Pour cela, on ajoute 64 bits par ''tile'' pour stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1.
[[File:Dxt23-memory-layout.png|centre|vignette|Dxt 2 et 3.]]
Dans le DXT4 et le DXT5, la méthode utilisée pour compresser les couleurs l'est aussi pour les valeurs de transparence. L'information de transparence est stockée par un en-tête contenant deux valeurs de transparence, le tout suivi d'une matrice qui attribue trois bits à chaque texel. En fonction de la valeur des trois bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1.
[[File:Dxt45-memory-layout.png|centre|vignette|Dxt 4 et 5.]]
===Le format de compression PVRTC===
Passons maintenant à un format de compression de texture un peu moins connu, mais pourtant omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle travaille surtout dans les cartes graphiques embarquées. Ses cartes se trouvent notamment dans l'ipad, l'iPhone, et bien d'autres smartphones actuels.
Avec le PVRTC, les textures sont encore une fois découpées en ''tiles'' de 4 texels par 4, mais la ressemblance avec le DXTC s’arrête là. Chacque ''tile'' est codée avec :
* une couleur codée sur 16 bits ;
* une couleur codée sur 15 bits ;
* 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
* et un bit de modulation, qui permet de configurer l’interprétation des bits de mélange.
Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4*4.
==Annexe : le ''normal-mapping'' hardware==
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, parlons un peu du ''normal mapping'' et de son implémentation dans les unités de texture. Pour rappel, 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. Et pour comprendre quel est le rapport avec les textures, nous allons devoir faire quelques rappels sur l'éclairage par pixel.
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
L'éclairage se fait en utilisant de nombreux calculs, qu'on a détaillé dans le chapitre sur les bases du rendu 3D. Ceux-ci utilisent la normale d'un sommet, à savoir un vecteur orienté à la verticale de la surface. En théorie, il y a une normale par sommet, ce qui fait que les calculs d'éclairage doivent se faire au niveau géométrique, avant la rastérisation. Mais pour obtenir un éclairage de meilleure qualité, il y a des techniques qui calcule l'éclairage d'une scène 3D pixel par pixel.
Une première technique d'éclairage par pixel interpole les normales lors de l'étape de rastérisation. On obtient alors un éclairage de Phong. Une autre solution est celle du ''normal mapping''. Le '''''normal mapping''''' précalcule les normales d'une surface dans une texture, appelée la ''normal map''. L'éclairage est alors réalisé par un pixel shader, qui lit les normales depuis cette texture et fait les calculs d'éclairage avec.
===Les ''normal maps'' sont des textures===
Notons que l'accès à la ''normal map'' se fait comme pour n'importe quelle texture : les texels sont lus en mémoire vidéo, puis le filtrage de texture est appliqué, et enfin le tout est envoyé au ''pixel shader''. En théorie, le filtrage effectue une sorte d'interpolation des normales automatique, ce qui permet de trouver la normale à un pixel précis, même s'il n'est pas sur un texel.
La ''normal map'' est une texture, donc. Mais ce n'est pas une texture comme une autre. Elle mémorise un vecteur pour chaque texel, pas une couleur. Il est possible d'utiliser les formats de textures vus plus haut, même si cela ne donne pas de bons résultats. Après tout, un vecteur est codé par trois coordonnées x,y,z, il est possible de coder chacune avec un octet et de packager cela dans une texture RGB classique. La coordonnée x va dans la composante Rouge, la coordonnée Y dans la composante Bleu, et la coordonnée z dans la composante z.
Le problème est que le rendu final n'est pas très beau. Et les opérations de filtrage ne donnent pas de très bons résultats. Pour remédier à ces problèmes, des formats de texture spécialisés pour les ''normal map'' ont été inventés. Le premier d'entre eux est le '''3Dc''', aussi appelé BC5 pour ''Block Compression 5''. Il utilise un octet par texel, au lieu de trois avec une texture RGB normale non-compressée.
===Le format de texture 3Dc===
Avec le 3Dc, les trois coordonnées des vecteurs ne sont pas mémorisées, seules deux d'entre elles le sont. En effet, les normales sont des vecteurs ''normalisés'', c'est à dire que leur longueur vaut 1, par construction. Vu que la taille est connue à l'avance, on peut en déduire la coordonnée z à partir des coordonnées x et y, avec l'usage du théorème de Pythagore. Par définition, <math>z^2 = 1 - x^2 - y^2</math>. Le calcul peut être fait dans le ''pixel shader'' sans problème.
: Pour être précis, il faut que les normales soient définies d'une certaine manière pour que ça marche. Les normales ne sont pas définies dans le même système de coordonnées que le modèle 3D, mais dans un autre système appelé l'''espace tangent''.
Pour le reste, la compression de la ''normal map'' ressemble un peu à celle des textures. La ''normal map'' est découpée en blocs de 4 par 4 texels de côté, chacune étant encodée en tenant compte de certaines redondances. Dans un bloc, les coordonnées x sont dans un certain intervalle, allant de <math>x_{min}</math> à <math>x_{max}</math>, idem pour la coordonnée y qui est dans l'intervalle <math>y_{min}</math>, <math>y_{max}</math>. L'intervalle <math>x_{min}</math> à <math>x_{max}</math> est coupé en 8 parts égales, ce qui fait 8 valeurs possibles dans cet intervalle, chacune correspondant à une valeur x possible pour une normale du bloc. La normale est donc encodée en précisant quelle valeur est la bonne, en utilisant un indice de 3 bits pour cela. La valeur exacte de la coordonnée x se calcule à partir de l'indice comme suit :
: <math>x = x_{min} + \left( \frac{x_{min} - x_{min}}{8} \right) \times \text{indice} </math>
Pour encoder un bloc de 4 pr 4 normales, il faut donc :
* Un octet pour chaque coordonnée <math>x_{min}</math>, <math>x_{max}</math>, <math>y_{min}</math>, <math>y_{max}</math>.
* 6 bits par normale : 3 pour encoder le décalage de la coordonnée x, 3 pour la coordonnée y.
Le tout permet d'encoder un bloc de 16 normales en seulement 128 bits, soit un octet par normale. Le ''pixel shader'' se débrouille pour décompresser un bloc. Il effectue notamment le calcul du dessus, pour retrouver la coordonnée x. Les calculs étant particulièrement simples, le cout en performance est très faible. Et ce d'autant plus qu'ils demandent juste de faire des additions et des multiplications entières, que l'unité scalaire peut faire en parallèle d'autres opérations plus gourmandes. Et le cout en calculs est de toute façon compensé par l'économie de mémoire et de bande passante lié à la compression : diviser la taille des données par trois, ca a un sacré impact !
==Annexe : les ''shadowmap'' hardware==
Les anciens GPU, notamment la Geforce FX, avaient des fonctionnalités spécifiques pour le calcul des ombres. Dans la plupart des jeux vidéos de l'époque, et même de maintenant, les ombres sont calculées avec la technique des ''shadowmap''. L'idée est assez simple sur le principe : un pixel est dans l'ombre quand il est invisible depuis une source de lumière.
L'idée est que le rendu est réalisé en plusieurs passes, avec une passe par source de lumière et une passe finale pour calculer l'image finale. Nous allons expliquer la technique avec une seule source de lumière, et allons utiliser l'exemple de la scène ci-dessous.
[[File:7fin.png|centre|vignette|Scène 3D d'exemple.]]
===La technique du ''shadowmapping''===
[[File:2shadowmap.png|vignette|Résultat de la première passe : ''shadowmap''..]]
La première passe rend l'image depuis le point de vue de la source de lumière. Cette première passe ne rend pas les couleurs de la scène, elle ne s'intéresse qu'à la profondeur des pixels. Le résultat est que l'image ne rend que le tampon de profondeur. Celui-ci est ensuite réutilisé comme texture pour la passe suivante. La texture en question est appelée la '''''shadownmap'''''.
La perspective utilisée, ainsi que le ''view frustrum'', dépend de la source de lumière. Pour une source de lumière qui émet un cône de lumière, le ''view frustrum'' de l'image rendue doit contenir tout le cone de lumière, et doit coller le plus possible à celui-ci. Pour une source directionnelle, comme le soleil, une perspective orthographique est utilisée.
La seconde passe rend l'image du point de vue de la caméra, pour rendre l'image finale. Elle rend l'image finale, qui est composée de pixels, chacun ayant une position à l'écran x,y, et une profondeur z. Les coordonnées sont transformées pour obtenir la position de ce pixel depuis le point de vue de la caméra. Une simple multiplication de matrice suffit, rien de bien compliqué, un shader peut le faire.
[[File:5failed.png|vignette|Résultat du test des comparaisons.]]
Après cette étape, on a alors les coordonnées x,y,z de ce pixel du point de vue de la caméra, et la ''shadowmap''. Il est alors possible d'accéder à la ''shadowmap'' au même endroit, à la même place que le pixel testé, aux mêmes coordonnées x,y. Si la profondeur du pixel est supérieure à celle de la shadowmap au même endroit, alors le pixel est situé derrière la surface visible, donc est dans l'ombre. Sinon, il n'est pas dans l'ombre. Le même procédé est répété sur chaque pixel de l'écran.
===Les optimisations hardware du ''shadowmapping''===
La technique des ''shadowmap'' demande donc de calculer une texture ''shadowmap'', puis de lire celle-ci et de faire des comparaisons de profondeur. Les GPU comme la Geforce FX intégraient du matériel dans les unités de texture pour faciliter ce travail. Les unités de texture pouvaient lire les ''shadowmap'', et faire la comparaison de profondeur toutes seules, elles avaient des circuits pour. Il suffisait de leur fournir le pixel à tester, ses coordonnées x,y,z, et l'adresse de la ''shadowmap''. Les unités de texture renvoyaient alors un résultat valant 0 ou 1 : 1 si le pixel est dans l'ombre, 0 sinon.
Elles pouvaient même effectuer du filtrage de texture sur les ''shadowmap''. Mais le filtrage était différent de celui utilisé sur les autres textures : moyenner des valeurs de profondeur ne marche pas bien. Elles utilisaient des techniques de filtrage différentes : elles faisaient les tests de comparaison, puis faisaient la moyenne des résultats. Ainsi, pour du filtrage bilinéaire, elles lisaient 4 texels dans la ''shadowmap'', puis faisaient 4 tests de comparaison, et moyennaient les 4 résultats.
==Annexe : les textures virtuelles==
Les '''textures virtuelles''' sont une optimisation des textures normales, qui visent à accélérer le rendu de terrains de grande taille. Imaginez par exemple un monde assez ouvert, comme un environnement en forêt ou en montagne, avec une grande distance de visibilité. Avec de tels terrains, le "sol" est recouvert par une texture de sol unique qui recouvre tout le terrain. Elle ne se répète pas, est de très grande taille, et peut parfois recouvrir toute la map ! Mais il n'y a pas assez de mémoire vidéo pour mémoriser la texture toute entière. La seule solution est la suivante : une partie de la texture est placée en mémoire vidéo, le reste est soit placé en mémoire RAM ou sur le disque dur.
Pour cela, le moteur de jeu utilise une optimisation ingénieuse, basée sur une observation assez basique : une bonne partie de la texture est visible, mais le reste est caché par des arbres, des habitations ou d'autres obstacles. Une optimisation possible de ne garder en mémoire vidéo que les portions visibles de la texture, pas les portions cachées. Une autre optimisation mélange textures virtuelles et ''mip-mapping''. L'idée est que pour les portions lointaines d'une texture, la texture utilisée est une ''mip-map'' de basse résolution. L'idée est alors de ne charger que la ''mip-map'' adéquate, pas les autres niveaux de détail. En clair, la texture de base n'est pas chargée en mémoire vidéo, mais la ''mip-map'' basse résolution l'est.
===Une texture à deux niveaux===
L'implémentation des textures virtuelles découpe les méga-textures en ''tiles'', en morceaux rectangulaires de taille modeste. En clair, le terrain est découpé en morceau rectangulaires/carrés. Seules les tiles nécessaires sont chargées en mémoire vidéo, pas les autres. Par exemple, les ''tiles'' non-visibles ne sont pas placées en mémoire vidéo, seules les ''tiles'' visibles le sont. De même, il y a une ''tile'' par niveau de mip-map : seul la tile correspondant le niveau adéquat est en mémoire vidéo, les autres niveaux de détail ne sont pas chargés. On peut faire une analogie avec la mémoire virtuelle, où les données sont découpées en pages, qui sont chargées en mémoire RAM à la demande, suivant les besoins, les données pouvant être swappées sur le disque dur si elles sont peu utilisées. Sauf qu'ici, il s'agit de textures qui sont découpées en pages chargées à la demande en mémoire vidéo, depuis la RAM système.
Une texture virtuelle est en réalité un système à deux niveaux : une liste de ''tiles'' et les ''tiles'' elles-mêmes. La liste de ''tiles'' est appelée un '''atlas de texture''', c'est un peu l'équivalent de la ''tilemap'' pour le rendu 2D. Rendre une texture demande de calculer quelle ''tile'' contient le texel à afficher, consulter la ''tile'' en question, puis récupérer le texel adéquat dans cette ''tile''. La ''tile'' est donc une texture, mais la texture à charger est choisie parmi un ensemble, qui est ici l'atlas de texture.
===L'implémentation : logicielle versus matérielle===
Les textures virtuelles ont été utilisées pour la première fois par les jeux Rage 1 et 2 d'IdSoftware, et quelques jeux ultérieurs comme DOOM 2016. IdSoftware les appelait des '''''mega-textures'''''. L'optimisation permettait des gains en performance assez impressionnants. Le jeu Rage 1 utilisait une texture carrée unique de 128k pixels de côté pour rendre le terrain. En théorie, une telle texture devrait prendre 64 giga-octets, mais le jeu tournait correctement avec 512 méga-octets de RAM, poussivement avec seulement 256 méga-octets de RAM.
De nos jours, les textures virtuelles sont supportées par beaucoup de jeux vidéos, les moteurs les plus courants gèrent de telles textures de manière logicielles. Mais quelques GPU récents supportent les textures virtuelles. Sur les GPU récents, l'atlas de texture est géré nativement par le matériel. Le GPU choisit quelle ''tile'', quelle texture choisir pour rendre le texel adéquat. Pour cela, le GPU calcule quelle ''tile'' charger, consulte l'atlas de texture, et lit la texture de ''tile'' adéquate.
Mais l'implémentation sur les GPU récents a de nombreuses limitations. La limitation la plus importante est que la taille des textures virtuelles ne peut pas dépasser la taille d'une texture normale, soit 32768 pixels de côté pour une texture carrée environ sur les GPU de 2020. De plus, le chargement d'une ''tile'' est très lent. En clair, dès qu'on veut changer de niveau de mip-map pour une tile, ou dès qu'une tile devient visible, le chargement de la tile peut facilement prendre plusieurs centaines de millisecondes. Le filtrage de texture est très complexe avec des textures virtuelles, ce qui fait que le filtrage de texture virtuelle est souvent soumis à des limitations que les textures normales n'ont pas, notamment pour le filtrage anisotropique.
{{NavChapitre | book=Les cartes graphiques
| prev=Le rasterizeur
| prevText=Le rasterizeur
| next=Les Render Output Target
| nextText=Les Render Output Target
}}{{autocat}}
bnkpn70ac3n0yok45if14ehaeev0li2
763413
763412
2026-04-10T17:52:31Z
Mewtow
31375
/* Le format de texture 3Dc */
763413
wikitext
text/x-wiki
[[File:Texture mapping.png|vignette|''Texture mapping'']]
Les '''textures''' sont des images que l'on va plaquer sur la surface d'un objet, du papier peint en quelque sorte. Les cartes graphiques supportent divers formats de textures, qui indiquent comment les pixels de l'image sont stockés en mémoire : RGB, RGBA, niveaux de gris, etc. Une texture est donc composée de "pixels", comme toute image numérique. Pour bien faire la différence entre les pixels d'une texture, et les pixels de l'écran, les pixels d'une texture sont couramment appelés des ''texels''.
==Le placage de textures inverse==
Pour rappel, plaquer une texture sur un objet consiste à attribuer un texel à chaque sommet, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture.
Dans les faits, on n'utilise pas de coordonnées entières de ce type. Les coordonnées de texture sont deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. Le nom donnée à cette technique de description des coordonnées de texture s'appelle l''''''UV Mapping'''''.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Les API 3D modernes gèrent des textures en trois dimensions, ce qui ajoute une troisième coordonnée de texture notée w. Dans ce qui va suivre, nous allons passer les textures en trois dimensions sous silence. Elles ne sont pas très utilisées, la quasi-totalité des jeux vidéo et applications 3D utilisant des textures en deux dimensions. Par contre, le matériel doit gérer les textures 3D, ce qui le rend plus complexe que prévu. Il faut ajouter quelques circuits pour, de quoi gérer la troisième coordonnée de texture, etc.
Lors de la rastérisation, chaque fragment se voit attribuer un sommet, et donc la coordonnée de texture qui va avec. Si un pixel est situé pile sur un sommet, la coordonnée de texture de ce sommet est attribuée au pixel. Si ce n'est pas le cas, la coordonnée de texture finale est interpolée à partir des coordonnées des trois sommets du triangle rastérisé. L'interpolation en question a lieu dans l'étape de rastérisation, comme nous l'avons vu dans le chapitre précédent. Le fait qu'il y ait une interpolation fait que les coordonnées du pixel gagent à être des nombres flottants. On pourrait faire une interpolation avec des coordonnées de texture entières, mais les arrondis et autres imprécisions de calcul donneraient un résultat graphiquement pas terrible, et empêcheraient d'utiliser les techniques de filtrage de texture que nous verrons dans ce chapitre.
À partir de ces coordonnées de texture, la carte graphique calcule l'adresse du texel qui correspond, et se charge de le lire. Et toute la magie a lieu dans ce calcul d'adresse, qui part de coordonnées de texture flottante, pour arriver à une adresse mémoire. Le calcul de l'adresse du texel se fait en plusieurs étapes, que nous allons voir ci-dessous. La première étape convertit les coordonnées flottantes en coordonnées entières, qui disent à quel ligne et colonne se trouve le texel voulu dans la texture. L'étape suivante transforme ces coordonnées x,y entières en adresse mémoire.
===La normalisation des coordonnées===
J'ai dit plus haut que les coordonnées de texture sont des coordonnées flottantes, comprises entre 0 et 1. Mais il faut savoir que les pixels shaders peuvent modifier celles-ci pour mettre en œuvre certains effets graphiques. Et le résultat peut alors se retrouver en-dehors de l'intervalle 0,1. C'est quelque chose de voulu et qui est traité par la carte graphique automatiquement, sans que ce soit une erreur. Au contraire, la manière dont la carte graphique traite cette situation permet d'implémenter des effets graphiques comme des textures en damier ou en miroir.
[[File:Clamp tile.jpg|vignette|Clamp tile]]
Il existe globalement trois méthodes très simples pour gérer cette situation, qui sont appelés des '''modes d'adressage de texture'''.
* La première méthode est de faire en sorte que le résultat sature. Si une coordonnée est inférieur à 0, alors on la remplace par un zéro. Si elle est supérieure à 1, on la ramène à 1. Avec cette méthode, tout se passe comme si les bords de la texture étaient étendus et remplissaient tout l'espace autour de la texture. Le tout est illustré ci-dessous. Ce mode d'accès aux textures est appelé le '''''clamp'''''.
* Une autre solution retire la partie entière de la coordonnée, elle coupe tout ce qui dépasse 1. Pour le dire autrement, elle calcule le résultat modulo 1 de la coordonnée. Le résultat est que tout se passe comme si la texture était répétée à l'infini et qu'elle pavait le plan.
* Une autre méthode remplit les coordonnées qui sortent de l’intervalle 0,1 avec une couleur préétablie, configurée par le programmeur.
===La conversion des coordonnées de textures flottantes en adresse mémoire===
Une fois la normalisation effectuée, les coordonnées de texture sont utilisées pour lire le texel voulu. Pour cela, les coordonnées de texte sont transformées en adresse mémoire, adresse qui pointe sur le texel ayant ces cordonnées. Pour cela, la première étape est de transformer les coordonnées flottantes u,v en coordonnées entières x,y qui pointent sur un texel. Pour cela, il suffit de multiplier les coordonnées flottantes u,v par la résolution de la texture accédée. Pour un écran de résolution <math>\text{height,width}</math>, le calcul est le suivant :
: <math>x = u \times \text{width}</math>
: <math>y = v \times \text{height}</math>
Le résultat est un nombre avec une partie entière et une partie fractionnaire. La partie entière des deux coordonnées donne la position x,y voulue, et la partie fractionnaire est conservée pour le filtrage de textures, mais passons cela sous silence pour le moment.
La seconde étape prend les coordonnées entières x,y et calcule l'adresse mémoire du texel. L'adresse dépend de la position de la texture en mémoire, précisément de son début, son premier texel, mais aussi de la position du texel par rapport au début de la texture. Et calculer cette position intra-texture dépend de la manière dont les texels sont stockés en mémoire.
====Les textures naïves====
Les programmeurs qui lisent ce cours s'attendent certainement à ce que la texture soit stockée en mémoire ligne par ligne, ou colonne par colonne. Cela veut dire que le premier pixel en partant d'en haut à gauche est stocké en premier, puis celui immédiatement à sa droite, puis celui encore à droite, et ainsi de suite. Une fois qu'on arrive à la fin d'une ligne, on passe à la ligne suivante, en-dessous. Cette organisation ligne par ligne s'appele l'organisation '''''row major order'''''. On peut faire pareil, mais colonne par colonne, ce qui donne le '''''column major order'''''.
[[File:Speicheranordnung Feld.svg|centre|vignette|upright=2|Row et column major order.]]
Maintenant, supposons que la texture commence à l'adresse <math>A_\text{texture}</math>, qui est l'adresse du premier texel. La texture a une résolution de <math>\text{width}</math> texels de large et <math>\text{height}</math> texels de haut. Par définition, les coordonnées X et Y des texels commencent à 0, ce qui fait que le pixel en haut à gauche a les coordonnées 0,0.
L'adresse du pixel se calcule comme suit :
: <math>A_\text{pixel} = A_\text{texture} + (\text{taille d'une ligne en octets} \times Y) + (\text{taille d'un texel en octets} \times X)</math>
La taille d'un pixel en mémoire est notée T. La taille d'une ligne en mémoire est de <math>width \times T</math>, par définition, vu qu'elle fait <math>width</math> texels. On a donc :
: <math>A_\text{pixel} = A_\text{texture} + (width \times T \times Y) + (T \times X)</math>
La formule se réécrit comme suit :
: <math>A_\text{pixel} = A_\text{texture} + T \times (width \times Y + X)</math>
Le calcul d'adresse est donc assez simple. Malheureusement, les textures ne sont pas stockées de cette manière en mémoire vidéo. En effet, elle se marie mal avec les opérations de filtrage de texture que nous allons voir dans ce qui suit. Le filtrage d'un texel dépend de ses voisins du dessus et du dessous. Le fait que la texture n'est pas forcément parcourue ligne par ligne fait que stocker une texture ligne par ligne n'est pas l'idéal.
De même, les textures sont déformées par la perspective. L'affichage de la texture ne se fait alors pas ligne par ligne, mais en parcourant la texture en diagonale, l'angle de la diagonale correspondant approximativement à l'angle que fait la verticale de la texture avec le regard. Vu qu'on ne connait pas à l'avance l'angle que fera la diagonale de parcours, on doit ruser.
====Les textures tilées====
Une première solution à ce problème est celle des '''textures tilées'''. Avec ces textures, l'image de la texture est découpée en ''tiles'', des rectangles ou en carrés de taille fixe, généralement des carrés de 4 pixels de côté. Les tiles ont une largeur et une longueur égales, afin de simplifier les calculs : on divise X et Y par le même nombre. De plus, leur largeur et leur longueur sont une puissance de deux, afin de simplifier les calculs d'adresse. Les ''tiles'' sont alors mémorisée les unes après les autres dans le fichier de la texture.
[[File:Texture tilée.png|centre|vignette|upright=2|Texture tilée]]
La formule de calcul d'adresse vue plus haut doit être adaptée pour tenir compte des tiles. Pour cela, il faut remplacer la taille d'un texel par la taille d'une tile, et que la largeur de la texture soit exprimée en nombre de tiles. De plus, on doit adapter les coordonnées des texels pour donner des coordonnées de tile. Généralement, les tiles sont des carrés de N pixels de côté, ce qui fait qu'on peut regrouper les lignes et les colonnes par paquets de N. Il suffit donc de diviser Y et X pour obtenir les coordonnées de la tile, de même que la larguer. La formule pour calculer la position de la énième tile est alors la suivante :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + \text{Taille mémoire d'une tile} \times \left( {\text{Width} \over N} \times {Y \over N} + {X \over N} \right)</math>
On peut réécrire le tout comme suit :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + K \times \left( {Y \over N} + X \right)</math>, avec K une constante connue à la compilation des shaders.
Vu que les tiles sont carrées avec une largeur qui est une puissance de deux, la multiplication par la taille d'une tile en mémoire se simplifie : on passe d'une multiplication entière à des décalages de bits. Même chose pour le calcul de l'adresse de la tile à partir des coordonnées x,y : ils impliquent des divisions par une puissance de deux, qui deviennent de simples décalages.
La position d'un pixel dans une tile dépend du format de la texture, mais peut se calculer avec quelques calculs arithmétiques simples. Dans les cas les plus simples, les pixels sont mémorisés ligne par ligne, ou colonne par colonne. Mais ce n'est pas systématiquement le cas. Toujours est-il que les calculs pour déterminer l'adresse sont simples, et ne demandent que quelques additions ou multiplications. Mais avec les formats de texture utilisés actuellement, les tiles sont chargées en entier dans le cache de texture, sans compter que diverses techniques de compression viennent mettre le bazar, comme on le verra dans la suite de cours.
Un avantage de l'organisation en tiles est qu'elle se marie bien avec le parcours des textures. On peut parcourir une texture dans tous les sens, horizontal, vertical, ou diagonal, on sait que les prochains pixels ont de fortes chances d'être dans la même tile. Si on rentre dans une tile par la gauche en haut, on a encore quelques pixels à parcourir dans la tile, par exemple. De même, le filtrage de textures est facilité. On verra dans ce qui va suivre que le filtrage de texture a besoin de lire des blocs de 4 texels, des carrés de 2 pixels de côté. Avec l'organisation en tile, on est certain que les 4 texels seront dans la même tile, sauf s'ils ont le malheur d'être tout au bord d'une tile. Ce dernier cas est assez rare, et il l'est d'autant plus que les tiles sont grandes. Enfin, un dernier avantage est que les tiles sont généralement assez petites pour tenir tout entier dans une ligne de cache. Le cache de texture est donc utilisé à merveille, ce qui rend les accès aux textures plus rapides.
====Les textures basées sur des ''z-order curves''====
Les formats de textures théoriquement optimaux utilisent une '''''Z-order curve''''', illustrée ci-dessous. L'idée est de découper la texture en quatre rectangles identiques, et de stocker ceux-ci les uns à la suite des autres. L'intérieur de ces rectangles est lui aussi découpé en quatre rectangles, et ainsi de suite. Au final, l'ordre des pixels en mémoire est celui illustré ci-dessous.
[[File:Z-CURVE.svg|centre|vignette|upright=2|Construction d'une ''Z-order curve''.]]
Les texels sont stockés les uns à la suite des autres dans la mémoire, en suivant l'ordre donnée par la ''Z-order curve''. Le calcul d'adresse calcule la position du texel en mémoire, par rapport au début de la texture, et ajoute l'adresse du début de la texture. Mais tout le défi est de calculer la position d'un texel en mémoire, à partir des coordonnées x,y. Le calcul peut sembler très compliqué, mais il n'en est rien. Le calcul demande juste de regarder les bits des deux coordonnées et de les combiner d'une manière particulièrement simple. Il suffit de placer le bit de poids fort de la coordonnée x, suivi de celui de la coordonnée y, et de faire ainsi de suite en passant aux bits suivants.
[[File:Zcurve45bits.png|centre|vignette|upright=1.5|Calcul de la position d'un élément dans une ''Z-order curve'' à partir des coordonnées x et y.]]
L'avantage d'une telle organisation est que la textures est découpées en ''tiles'' rectangulaires d'une certaine taille, elles-mêmes découpées en ''tiles'' plus petites, etc. Et il se trouve que cette organisation est parfaite pour le cache de texture. L'idéal pour le cache de texture est de charger une ''tile'' complète dans le cache de textures. Quand on accède à un texel, on s'assure que la ''tile'' complète soit chargée. Mais cela demande de connaitre à l'avance la taille d'une ''tile''. Les formats de texture fournissent généralement une ''tile'' carré de 4 pixels de côté, mais cela donnerait un cache trop petit pour être vraiment utile. Avec cette méthode, on s'assure qu'il y ait une ''tile'' avec la taille optimale. Les ''tiles'' étant découpées en ''tiles'' plus petites, elles-mêmes découpées, et ainsi de suite, on s'assure que la texture est découpées en ''tiles'' de taille variées. Il y aura au moins une ''tile'' qui rentrera tout pile dans le cache.
==Les techniques de rendu à textures multiples==
Nous venons de voir comment une texture est plaquée sur un objet 3D, ou une surface comme un sol. Pour résumer, le calcul de l'adresse d'un texel prend la position du texel par rapport au début de la texture, et ajoute l'adresse du début de la texture. L'adresse mémoire de la texture est connue au moment où le pilote de la carte graphique place la texture dans la mémoire vidéo, et cette information est transmise au matériel par l'intermédiaire du processeur de commande, puis passée aux processeurs de shaders et à l'unité de texture. Le tout est couplé à d'autres informations, la plus importante étant la ''taille de la texture en octets'', pour éviter de déborder lors des accès à la texture.
Néanmoins, il s'agit là du cas le plus simple. Certaines techniques de rendu demandent de choisir la texture à plaquer parmi un ensemble de plusieurs textures. Les techniques en question sont assez variées et n'ont pas grand chose en commun. Les plus connues sont le ''mip-mapping'', le ''cube-mapping'' et les textures virtuelles. Le ''mip-mapping'' sert à filtrer les textures, chose qu'on expliquera plus tard, le ''cube-mapping'' sert à simuler des réflexions sur un objet en plaquant une texture de l'environnement dessus, les textures virtuelles sont une optimisation pour les textures des terrains de grande taille. Mais malgré leurs différences, elles demandent de choisir quelle texture plaquer entre plusieurs textures de base. En clair, l'adresse de base de la texture varie selon la situation. Voyons-les dans le détail.
===Le mip-mapping===
Le '''mip-mapping''' a pour but de légèrement améliorer les graphismes des objets lointains, tout en rendant les calculs de texture plus rapides. Formellement, le ''mip-mapping'' est une technique de filtrage de texture, mais nous l'abordons maintenant car elle est surtout liée au calcul d'adresse. Les unités de texture ont des circuits de filtrage de texture séparés des circuits de ''mip-mapping'' et de calcul d'adresse, d'où le fait que nous en parlons séparément.
Le problème résolu par le ''mip-mapping'' est le rendu des textures lointaines. Si une texture est plaquée sur un objet lointain, une bonne partie des détails est invisible pour l'utilisateur. Un pixel de l'écran est associé à plusieurs texels. Idéalement, la carte graphiques devrait lire tous ces texels et en faire une sorte de moyenne pondérée, pour calculer la couleur finale du pixel. Mais dans les faits, ce serait très gourmand et compliqué à implémenter en hardware. Une solution serait de ne garder que quelque texels, mais cela a tendance à créer des artefacts visuels (les textures affichées ont tendance à pixeliser). Le ''mip-mapping'' permet de réduire ces deux problèmes en même temps en précalculant cette moyenne pondérée pour des distances prédéfinies.
L'idée est d'utiliser plusieurs exemplaires d'une même texture à des résolutions différentes, chaque exemplaire étant adapté à une certaine distance. Par exemple, une texture sera stocké avec un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre de 128 * 128 et ainsi de suite jusqu’à un dernier exemplaire de 32 * 32 pixel. Chaque exemplaire correspond à un '''niveau de détail''', aussi appelé ''Level Of Detail'' (abrévié en LOD). La résolution utilisée diminue d'autant plus que l'objet est situé loin de la caméra. Les objets proches seront rendus avec la texture 512*512, ceux plus lointains seront rendus avec la texture de résolution 256*256, les textures 128*128 seront utilisées encore plus loin, et ainsi de suite jusqu'aux objets les plus lointains qui sont rendus avec la texture la plus petite de 32*32.
[[File:MipMap Example STS101.jpg|centre|vignette|upright=2|Exemples de mip-maps.]]
Le ''mip-mapping'' améliore grandement la qualité d'image. L'image d'exemple ci-dessous le montre assez bien.
[[File:Mipmapping example.png|centre|vignette|upright=2|Exemple de mipmapping.]]
Pour faciliter les calculs d'adresse, les LOD d'une même texture sont stockées les uns après les autres en mémoire (dans un tableau, comme diraient les programmeurs). Ainsi, pas besoin de se souvenir de la position en mémoire de chaque LOD : l'adresse de la texture de base, et quelques astuces arithmétiques suffisent. Prenons le cas où la texture de base a une taille L. le premier exemplaire est à l'adresse 0, le second niveau de détail est à l'adresse L, le troisième à l'adresse L + L/4, le suivant à l'adresse L + L/4 + L/16, et ainsi de suite. Le calcul d'adresse demande juste connaître le niveau de détails souhaité et l'adresse de base de la texture. Le niveau de détail voulu est calculé par les pixel shaders, en fonction de la coordonnée de profondeur du pixel à traiter.
Évidemment, cette technique consomme de la mémoire vidéo, vu que chaque texture est dupliquée en plusieurs exemplaires, en plusieurs LOD. Dans le détail, la technique du mip-mapping prend au maximum 33% de mémoire en plus (sans compression). Cela vient du fait qu'en prenant une texture dexu fois plus petite, elle prend 4 fois moins de mémoire : 2 fois moins de pixels en largeur, et 2 fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des LODs, texture de base comprise, prendra X + (X/4) + (X/16) + (X/256) + … Un petit calcul de limite donne 4/3 * X, soit 33% de plus.
===Le cube-mapping===
[[File:Cube mapped reflection example 2.JPG|vignette|Exemple de reflets environnementaux.]]
L''''environnement-mapping''' est une technique de calcul de divers effets graphiques liés à l'environnement, notamment des réflexions. L'idée est de plaquer une texture pré-calculée pour simuler l'effet de l'environnement sur une surface ou un objet 3D. Il en existe plusieurs versions différentes, mais la seule utilisée de nos jours est le ''cube-mapping'', où la texture de l'environnement est plaquée sur un cube, d'où son nom. Le cube en question est utilisé différemment suivant ce que l'on cherche à faire avec le ''cube-mapping''. Les deux utilisations principales sont le rendu du ciel et des décors, et les réflexions sur la surface des objets. Dans les deux cas, l'idée est de précalculer ce que l'on voit du point de vue de la caméra. On place la caméra dans la scène 3D, on place un cube centré sur la caméra, le cube est texturé avec ce que l'on voit de l'environnement depuis la caméra/l'objet de son point de vue.
[[File:Panorama cube map.png|centre|vignette|upright=2|L'illustration montre en premier lieu une ''cubemap'' avec les six faces mises en évidence, puis quel environnement 3D elle permet de simuler, le troisième illustration montrant comment la ''cubemap'' est utilisée pour simuler l'environnement.]]
Le rendu du ciel et des décors lointains dans les jeux vidéo se base sur des '''''skybox''''', à savoir un cube centré sur la caméra, sur lequel on ajoute des textures de ciel ou de décors lointains. Le cube est recouvert par une texture, qui correspond à ce que l'on voit quand on dirige le regard de la caméra vers cette face. Contrairement à ce qu'on pourrait croire, la skybox n'est pas les limites de la scène 3D, les limites du niveau d'un jeu vidéo ou quoique ce soit d'autre de lié à la physique de la scène 3D. La skybox est centrée sur la caméra, elle suit la caméra dans son mouvement. Centrer la skybox sur la caméra permet de simuler des décors très lointains, suffisamment lointain pour qu'on n'ait pas l'illusion de s'en rapprocher en se déplaçant dans la map. De plus, cela évite d'avoir à faire trop de calculs à chaque fois que l'on bouge la caméra. La texture plaquée sur le cube est une texture unique, elle-même découpée en six sous-textures, une par face du cube.
[[File:Skybox example.png|centre|vignette|upright=2|Exemple de Skybox.]]
[[File:Cube mapped reflection example.jpg|vignette|Réflexions calculées par une ''cubemap''.]]
Le ''cube-mapping'' est aussi utilisé pour des reflets. L'idée est de simuler les reflets en plaquant une texture pré-calculée sur l'objet réflecteur. La texture pré-calculée est un dessin de l'environnement qui se reflète sur l'objet, un dessin du reflet à afficher. En la plaquant la texture sur l'objet, on simule ainsi des reflets de l'environnement, mais on ne peut pas calculer d'autres reflets comme les reflets objets mobiles comme les personnages. Et il se trouve que la texture pré-calculée est une ''cubemap''. Pour les environnements ouverts, c'est la ''skybox'' qui est utilisée, ce qui permet de simuler les reflets dans les flaques d'eau ou dans des lacs/océans/autres. Pour les environnements intérieurs, c'est une cubemap spécifique qui utilisée. Par exemple, pour l'intérieur d'une maison, on a une ''cubemap'' par pièce de la maison. Les reflets se calculent en précisant quelle ''cubemap'' appliquer sur l'objet en fonction de la direction du regard.
[[File:Cube map level.png|centre|vignette|Cube map de l'intérieur d'une pièce d'un niveau de jeux vidéo.]]
Toujours est-il que les textures utilisées pour le ''cubemmapping'', appelées des ''cubemaps'', sont en réalité la concaténation de six textures différentes. En mémoire vidéo, la ''cubemap'' est stockée comme six textures les unes à la suite des autres. Lors du rendu, on doit préciser quelle face du cube utiliser, ce qui fait 6 possibilités. On a le même problème qu'avec les niveaux de détail, sauf que ce sont les faces d'une ''cubemap'' qui remplacent les textures de niveaux de détails. L'accès en mémoire doit donc préciser quelle portion de la ''cubemap'' il faut accéder. Et l'accès mémoire se complexifie donc. Surtout que l'accès en question varie beaucoup suivant l'API graphique utilisée, et donc suivant la carte graphique.
Les API 3D assez anciennes ne gérent pas nativement les ''cubemaps'', qui doivent être émulées en logiciel en utilisant six textures différentes. Le pixel shader décide donc quelle ''cubemap'' utiliser, avec quelques calculs sur la direction du regard. L'accès se fait d'une manière assez simple : le shader choisit quelle texture utiliser. Les API 3D récentes gèrent nativement les ''cubemaps''. Dans le cas le plus simple,pour les versions les plus vielles de ces API, les six faces sont numérotées et l'accès à une ''cubemap'' précise quel face utiliser en donnant son numéro. La carte graphique choisit alors automatiquement la bonne texture, mais cela demande de laisser le calcul de la bonne face au pixel shader. D'autres API 3D et cartes graphiques font autrement. Dans les API 3D modenres, les ''cubemap'' sont gérées comme des textures en trois dimensions, adressées avec trois coordonnées u,v,w. La carte graphique utilise ces trois coordonnées de manière à en déduire quelle est la face pertinente, mais aussi les coordonnées u,v dans la texture de la face.
==L'implémentation matérielle du placage de textures==
Pour résumer, la lecture d'un texel demande d'effectuer plusieurs étapes. Dans le cas le plus simple, sans ''mip-mapping'' ou ''cubemapping'', on doit effectuer les étapes suivantes :
* Il faut d'abord normaliser les coordonnées de texture pour qu'elles tombent dans l'intervalle [0,1] en fonction du mode d'adressage désiré.
* Ensuite, les coordonnées u,v doivent être converties en coordonnées entières, ce qui demande une multiplication flottante.
* Enfin, l'adresse finale est calculée à partir des coordonnées entières et en ajoutant l'adresse de base de la texture (et éventuellement avec d'autres calculs arithmétiques suivant le format de la texture).
Tout cela pourrait être fait par le pixel shaders, mais cela implique beaucoup de calculs répétitifs et d'opérations arithmétiques assez lourdes, avec des multiplications flottantes, des additions et des multiplications entières, etc. Faire faire tous ces calculs par les shaders serait couteux en performance, sans compter que les shaders deviendraient plus gros et que cela aurait des conséquences sur le cache d'instruction. De plus, certaines de ces étapes peuvent se faire en parallèle, comme les deux premières, ce qui colle mal avec l'aspect sériel des shaders.
Aussi, les processeurs de shaders incorporent une unité de calcul d'adresse spéciale pour faire ces calculs directement en matériel. L'unité de texture contient au minimum deux circuits : un circuit de calcul d'adresse, et un circuit d'accès à la mémoire. Toute la difficulté tient dans le calcul d'adresse, plus que dans le circuit de lecture. Le calcul d'adresse est conceptuellement réalisé en deux étapes. La première étape qui transforme les coordonnées u,v en coordonnées x,y qui donne le numéro de la ligne et de la colonne du texel dans la texture. La seconde étape prend ces deux coordonnées x,y, l'adresse de la texture, et détermine l'adresse de la tile à lire.
[[File:Unité de texture simple.png|centre|vignette|upright=2|Unité de texture simple]]
===L'implémentation du mip-mapping===
Le ''mip-mapping'' est lui aussi pris en charge par l'unité de calcul d'adresse, car cette technique change l'adresse de base de la texture. La gestion du ''mip-mapping'' est cependant assez complexe. Il est possible de laisser le pixel shader calculer quel niveau de détail utiliser, en fonction de la coordonnée de profondeur z du pixel à afficher. La carte graphique détermine alors automatiquement quelle texture lire, quel niveau de détail, automatiquement. Elle détermine aussi la bonne résolution pour la texture, qui est égal à la résolution de la texture de base, divisée par le niveau de détail. Pour résumer, le niveau de détail est envoyé aux unités de texture, qui s'occupent de calculer l'adresse de base et la résolution adéquates. Quelques calculs arithmétiques simples, donc, qui s'implémentent facilement avec quelques circuits.
Mais une autre méthode laisse la carte graphique déterminer le niveau de détail par elle-même. Dans ce cas, cela demande, outre les deux coordonnées de texture, de calculer la dérivée de ces deux coordonnées dans le sens horizontal et vertical, ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Les quatre dérivées sont les suivantes :
: <math>\frac{du}{dx}</math>, <math>\frac{dv}{dx}</math>, <math>\frac{du}{dy}</math>, <math>\frac{dv}{dy}</math>
Un bon moyen pour obtenir les dérivées demande de regrouper les pixels par groupes de 4 et de faire la différence entre leurs coordonnées de texture respectives. On peut calculer les deux dérivées horizontales en comparant les deux pixels sur la même ligne, et les deux dérivées verticales en comparant les deux pixels sur la même colonne. Mais cela demande de rastériser les pixels par groupes de 4, par ''quads''. Et c'est ce qui est fait sur les cartes graphiques actuelles, qui rastérisent des groupes de 4 pixels à la fois.
[[File:Texture sampler unit with mipmapping.png|centre|vignette|upright=2.0|Unité de texture avec mipmapping.]]
Malheureusement, le calcul exact utilisé pour le choix de la mip-map dépend du GPU considéré et peu de chose est connu quant à ces algorithmes. Il est possible d'inférer le comportement à partir d'observations, mais guère plus. Pour ceux qui veulent en savoir plus, je conseille la lecture de cet article de blog :
* [https://pema.dev/2025/05/09/mipmaps-too-much-detail/ Mipmap selection in too much detail]
===La gestion des accès mémoire===
Enfin, l'unité de texture doit tenir compte du fait que la mémoire vidéo met du temps à lire une texture. En théorie, l'unité de texture ne devrait pas accepter de nouvelle demande de lecture tant que celle en cours n'est pas terminée. Mais faire ainsi demanderait de bloquer tout le pipeline, de l'''input assembler'' au unités de''shaders'', ce qui est tout sauf pratique et nuirait grandement aux performances.
Une solution alternative consiste à mettre en attente les demandes de lectures de texture pendant que la mémoire est occupée. La manière la plus simple d'implémenter des accès mémoire multiples est de les mettre en attente dans une petite mémoire FIFO. Cela implique que les accès mémoire s’exécutent dans l'ordre demandé par le ''shader'' et/ou l'unité de rastérisation, il n'y a pas de réorganisation des accès mémoire ou d’exécution dans le désordre des accès mémoire.
[[File:Texture prefetching.png|centre|vignette|upright=1.5|Accès mémoire simultanés.]]
Évidemment, quand la mémoire FIFO est pleine, le pipeline est alors totalement bloqué. Le rasteriser est prévenu que l'unité de texture ne peut pas accepter de nouvelle lecture de texture. En pratique, la FIFO est généralement d'une taille respectable et permet de mettre en attente beaucoup de demandes de lecture de texture. Il faut de plus noter qu'il y a une FIFO par processeur de ''shader'' sur les cartes graphiques modernes. Quand elle est pleine, le processeur cesse d'exécuter de nouveaux accès mémoire, mais peut continuer à exécuter des ''shaders'' dans les autres unités de calcul, pas besoin de bloquer complétement le pipeline.
===L'intégration du cache de textures===
Il faut noter que les unités de texture incorporent aussi un cache de texture, voire plusieurs. L'intégration des caches de texture avec la mémoire FIFO précédente est quelque peu compliqué, car il faut garantir que les lectures de texture se fassent dans le bon ordre. On ne peut pas exécuter une lecture dans le cache alors que des lectures précédentes sont en attente de lecture en mémoire vidéo. Et cela pose un gros problème : une lecture dans le cache de texture prend quelques dizaines de cycles d'horloge, alors qu'une lecture en mémoire vidéo en prend facilement 400 à 800 cycles, parfois plus. Et cela fait que l'ordre des accès mémoire peut s'inverser.
Prenons par exemple un accès au cache précédé et suivi par deux accès en mémoire vidéo. Le premier démarre au cycle 1, et se termine au cycle numéro 400. L'accès au cache commence au cycle 2 et se termine 20 cycles après, au cycle numéro 22. En clair, la lecture dans le cache s'est terminée avant l'accès mémoire qui le précède. Les textures ne sont donc plus lues dans l'ordre. Et il faut trouver une solution pour éviter cela.
La solution est de retarder les lectures dans le cache tant que tous les accès précédents ne sont pas terminés. Mais pour retarder les lectures en question, il faut d'abord savoir si la lecture atterrit dans le cache ou non, ce qui demande d'accéder au cache. On fait face à un dilemme : on veut retarder les accès au cache, mais les différencier des lectures déclenchant des accès mémoire demande d'accéder au cache en premier lieu. La solution est décrite dans l'article "Prefetching in a Texture Cache Architecture" par Igehy et ses collègues. Elle se base sur deux idées combinées ensemble.
La première idée est de séparer l'accès au cache en deux : une étape qui vérifie si les texels à lire sont dans le cache, et une étape qui accède aux données dans le cache lui-même. Un cache de texture est donc composé de deux circuits principaux. Le premier vérifie la présence des texels dans le cache. Il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Ensuite, en plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
La seconde idée est de retarder l'accès au cache entre les deux phases. La première étape d'un accès mémoire vérifie si la donnée est dans le cache ou non. Puis, on retarde la lecture des données, pour attendre que toutes les lectures précédentes soient terminées. Et enfin, troisième étape : la lecture des texels dans la mémoire cache proprement dite. Les accès mémoire passant par la mémoire vidéo se font de la même manière, à une différence près : la lecture dans le cache est remplacée par la lecture en mémoire vidéo. Tout démarre avec une demande à l'unité de tags, qui vérifie si le texel est dans le cache ou non. Puis on retarde l'accès tant que la mémoire vidéo est occupée, puis on effectue la lecture en mémoire vidéo.
Si ce n'est pas le cas, l'accès mémoire est envoyé à la mémoire vidéo comme précédemment, à savoir qu'il est mis en attente dans une mémoire FIFO, puis envoyé à la mémoire vidéo dès que celle-ci est libre. Mais en sortie de la mémoire, la donnée lue est envoyée dans le cache de texture, par dans l'unité de filtrage. Pour savoir où placer la donnée lue, l'unité de tag a réservé une ligne de cache précise, une adresse bien précise. L'adresse en question est disponible en lisant une autre mémoire FIFO, qui a mis en attente l'adresse en question, en attendant que l'accès mémoire se termine. La donnée est alors écrite dans le cache, puis lue par l'unité de filtrage de textures.
Pour une lecture dans le cache, le déroulement est similaire, mais sans le passage par la mémoire. La lecture fait une demande à l'unité de tag, et celle-ci répond que la donnée est bien dans le cache. Elle place alors l'adresse à lire dans la file d'attente. Une fois que les accès mémoire précédents sont terminés, l'adresse sort de la file d'attente et est envoyée à la mémoire de données. La lecture s'effectue, les texels sont envoyés à l'unité de filtrage de textures. La seule différence avec un ''phased cache'' normal est l'insertion de l'adresse à lire dans une FIFO qui vise à mettre en attente
[[File:Unité de texture avec un cache de texture.png|centre|vignette|upright=2.0|Unité de texture avec un cache de texture]]
Pour résumer, l'implémentation précédente garantit une exécution des lectures dans leur ordre d'arrivée. Et pour cela, elle retarde les lectures dans le cache tant que les lectures en mémoire précédentes ne sont pas terminées. L'accès au cache est plus rapide que l'accès en mémoire vidéo, mais le retard ajouté pour garantir l'ordre des lectures fait que le temps d'accès est très long.
==Le filtrage de textures==
Plaquer des textures sans autre forme de procès ne suffit pas à garantir des graphismes d'une qualité époustouflante. La raison est que les sommets et les texels ne tombent pas tout pile sur un pixel de l'écran : le sommet associé au texel peut être un petit peu trop en haut, ou trop à gauche, etc. Une explication plus concrète fait intervenir les coordonnées de texture. Souvenez-vous que lorsque l'on traduit une coordonnée de texture u,v en coordonnées x,y, on obtient un résultat qui ne tombe pas forcément juste. Souvent, le résultat a une partie fractionnaire. Si celle-ci est non-nulle, cela signifie que le texel/sommet n'est pas situé exactement sur le pixel voulu et que celui-ci est situé à une certaine distance. Concrètement, le pixel tombe entre quatre texels, comme indiqué ci-dessous.
[[File:Filtrage texture.png|centre|vignette|upright=2.0|Position du pixel par rapport aux texels.]]
Pour résoudre ce problème, on doit utiliser différentes techniques d'interpolation, aussi appelées techniques de '''filtrage de texture''', qui visent à calculer la couleur du pixel final en fonction des texels qui l'entourent. Il existe de nombreux types de filtrage de textures, qu'il s'agisse du filtrage linéaire, bilinéaire, trilinéaire, anisotropique et bien d'autres.
Tous ont besoin d'avoir certaines informations qui sont généralement fournies par les circuits de calcul d'adresse. La première est clairement la partie fractionnaire des coordonnées x,y. La seconde est la dérivée de ces deux coordonnées dans le sens horizontal et vertical., ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Toujours est-il que le filtrage de texture est une opération assez lourde, qui demande beaucoup de calculs arithmétiques. On pourrait en théorie le faire dans les pixels shaders, mais le cout en performance serait absolument insoutenable. Aussi, les cartes graphiques intègrent toutes un circuit dédié au filtrage de texture, le ''texture sampler''. Même les plus anciennes cartes graphiques incorporent une unité de filtrage de texture, ce qui nous montre à quel point cette opération est importante.
[[File:Texture unit.png|centre|vignette|upright=2.0|Unité de texture.]]
On peut configurer la carte graphique de manière à ce qu'elle fasse soit du filtrage bilinéaire, soit du filtrage trilinéaire, on peut configurer le niveau de filtrage anisotropique, etc. Cela peut se faire dans les options de la carte graphique, mais cela peut aussi être géré par l'application. La majorité des jeux vidéos permettent de régler cela dans les options. Ces réglages ne concernent pas la texture elle-même, mais plutôt la manière dont l'unité de texture doit fonctionner. Ces réglages sur l''''état de l'unité de texture''' sont mémorisés quelque part, soit dans l'unité de texture elle-même, soit fournies avec la ressource de texture elle-même, tout dépend de la carte graphique. Certaines cartes graphiques mémorisent ces réglages dans les unités de texture ou dans le processeur de commande, et tout changement demande alors de réinitialiser l'état des unités de texture, ce qui prend un peu de temps. D'autres placent ces réglages dans les ressources de texture elles-mêmes, ce qui rend les modifications de configuration plus rapides, mais demande plus de circuits. D'autres cartes graphiques mélangent les deux options, certains réglages étant globaux, d'autres transmis avec la texture. Bref, difficile de faire des généralités, tout dépend du matériel et le pilote de la carte graphique cache tout cela sous le tapis.
Maintenant que cela est dit, voyons quelles sont les différentes méthodes de filtrage de texture et comment la carte graphique fait pour les calculer.
===Le filtrage au plus proche===
La méthode de filtrage la plus simple consiste à colorier avec le texel le plus proche. Cela revient tout simplement à ne pas tenir compte de la partie fractionnaire des coordonnées x,y, ce qui est très simple à implémenter en matériel. C'est ce que l'on appelle le '''filtrage au plus proche''', aussi appelé ''nearest filtering''.
Autant être franc, le résultat est assez pixelisé et peu agréable à l’œil. Par contre, le résultat est très rapide à calculer, vu qu'il ne demande aucun calcul à proprement parler. Elle ne fait pas appel à la parti fractionnaire des coordonnées entières de texture, ni aux dérivées de ces coordonnées. On peut combiner cette technique avec le mip-mapping, ce qui donne un résultat bien meilleur, bien que loin d'être satisfaisant. Au passage, toutes les techniques de filtrage de texture peuvent se combiner avec du mip-mapping, certaines ne pouvant pas faire sans.
[[File:Interpolation-nearest.svg|centre|vignette|Filtrage de texture au plus proche.]]
===Le filtrage linéaire===
Le filtrage le plus simple est le '''filtrage linéaire'''. Il effectue une interpolation linéaire entre deux mip-maps, deux niveaux de détails. Pour comprendre l'idée, nous allons prendre une situation très simple, avec une texture carrée de 512 texels de côté. Le mip-mapping crée plusieurs textures : une de 256 texels de côté, une de 128 texels, une de 64, etc. Maintenant, la texture est sur un objet à une certaine distance de l'écran, vu de face. Le résultat est qu'elle correspond à l'écran à un carré de 300 pixels de côté (pas d'erreur : pixels, pas texels). Dans ce cas, la texture se trouve entre deux mip-maps : celle de 512 pixels de côté, celle de 256. Laquelle choisir ? Le filtrage au plus proche prend la texture de 512 pixels de côté. Le filtrage linéaire lui, fait autrement.
Vu que la texture est entre deux mip-maps, l'idée est de prendre le texel au plus proche dans chaque texture et de faire une sorte de moyenne appelée l'interpolation linéaire. L'interpolation par du principe que la couleur varie entre les deux texels en suivant une fonction affine, illustrée ci-dessous. Ce ne serait évidemment pas le cas dans le monde réel, mais on supposer cela donne une bonne approximation de ce à quoi ressemblerait une texture à plus haute résolution. On peut alors calculer la couleur du pixel par une simple moyenne pondérée par la distance. Le résultat est que les transitions entre deux niveaux de détails sont plus lisses, moins abruptes.
[[File:Lin interp -é.png|centre|vignette|upright=2.0|Interpolation linéaire.]]
===Le filtrage bilinéaire===
Le filtrage bilinéaire effectue une sorte de moyenne pondérée des quatre texels les plus proches du pixel à afficher. Pour cela, rappelez-vous ce qui a été dit plus haut : les coordonnées x,y d'un pixel ont une partie entière et une partie fractionnaire. Le filtrage au plus proche élimine les parties fractionnaires, ce qui donne une coordonnée x,y. Avec le filtrage bilinéaire, on prend les texels de coordonnées (x,y) ; (x+1,y) ; (x,y+1) ; (x+1,y+1), le pixel étant entre ces 4 texels.
Mais le filtrage ne fait pas qu'une simple moyenne, il prend en compte les parties fractionnaires pour faire la moyenne. En effet, le pixel n'est pas au milieu du carré de texel, il est quelque part mais est souvent plus proche d'un texel que des autres. Et il faut donc pondérer la moyenne par les distances aux 4 texels. Pour cela, la moyenne est calculée à partir d'interpolations linéaires. Avec 4 pixels, nous allons devoir calculer la couleur de deux points intermédiaires. La couleur de ces deux points se calcule par interpolation linéaire, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.
[[File:Bilin3.png|centre|vignette|upright=2|Filtrage bilinéaire de texture.]]
Le circuit qui permet de faire l'interpolation bilinéaire est particulièrement simple. On trouve un circuit de chaque pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu, et un pour la transparence. Chacun de ces circuit est composé de sous-circuits chargés d'effectuer une interpolation linéaire, reliés comme suit.
[[File:Texture sampler unit.png|centre|vignette|Unité de filtrage bilinéaire.]]
Vous noterez que le filtrage bilinéaire accède à 4 pixels en même temps. Fort heureusement, les textures sont stockées de manière à ce qu'on puisse charger les 4 pixels en une fois, comme on l'a vu plus haut. Le filtrage bilinéaire a de fortes chances que les 4 pixels filtrés soient dans la même ''tile'', la seule exception étant quand ils sont tout juste sur le bord d'une ''tile''.
: La console de jeu Nintendo 64 n'utilise que trois pixels au lieu de quatre dans son interpolation bilinéaire, qui en devient une interpolation quasi-bilinéaire. La raison derrière ce choix est une question de performances, comme beaucoup de décisions de ce genre. Le résultat est un rendu imparfait de certaines textures.
===Le filtrage trilinéaire===
Avec le filtrage bilinéaire, des discontinuités apparaissent sur certaines surfaces. Par exemple, pensez à une texture de sol : elle est appliquée plusieurs fois sur toute la surface du sol. A une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512*512 à 256*256, ce qui est visible pour un joueur attentif. De telles transitions sont lissées grâce au filtrage linéaire, il n'y a plus qu'à le combiner avec le filtrage bilinéaire. Rien d’incompatible : le premier filtre l'intérieur d'une mip-map, le second combine deux mip-maps.
Le filtrage trilinéaire prend les deux mip-maps les plus proches, fait un filtrage bilinéaire avec chacune, puis fait une « une moyenne » pondérée entre les deux résultats. Le circuit de filtrage trilinéaire existe en plusieurs versions. La plus simple, illustrée ci-dessous, effectue deux filtrages bilinéaires en parallèle, dans deux circuits séparés, puis combine leurs résultats avec un circuit d'interpolation linéaire. Mais ce circuit nécessite de charger 8 texels simultanément. Qui plus est, ces 8 texels ne sont pas consécutifs en mémoire, car ils sont dans deux niveaux de détails/mip-maps différents.
[[File:Parallel trilinear filtering.png|centre|vignette|upright=2.0|Unité de filtrage trilinéaire parallèle.]]
Vu qu'on lit des texels dans deux mip-maps, les texels sont lus en deux fois : 4 texels provenant de la première mip-map, suivis par les 4 texels de l'autre mip-map. Les 4 premiers texels doivent donc être mis en attente dans des registres, en attendant que les 4 autres arrivent. Une amélioration du circuit précédent gère cela en ajoutant des registres. Il lit les 4 premiers texels, les filtre avec une interpolation bilinéaire, et mémorise le résultat dans un registre. Puis, il lit les 4 autres texels, les filtre, et met le résultat dans un second registre. A ce moment là, un circuit d'interpolation linéaire finit le travail. On économise donc un circuit d'interpolation bilinéaire, sans que les performances soient trop impactées.
[[File:Filtrage trilineaire.png|centre|vignette|upright=1.0|Unité de filtrage trilineaire série.]]
Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le temps où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. À chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients, et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.
===Le filtrage anisotrope===
D'autres artefacts peuvent survenir lors de l'application d'une texture, la perspective pouvant déformer les textures et entraîner l'apparition de flou. La raison à cela est que les techniques de filtrage de texture précédentes partent du principe que la texture est vue de face. Prenez une texture carrée, par exemple. Vue de face, elle ressemble à un carré sur l'écran. Mais tournez la caméra, de manière à voir la texture de biais, avec un angle, et vous verrez que la forme de la texture sur l'écran est un trapèze, pas un carré. Cette déformation liée à la perspective n'est pas prise en compte par les méthodes de filtrage de texture précédentes. Pour le dire autrement, les techniques de filtrage précédentes partent du principe que les 4 texels qui entourent un pixel forment un carré, ce qui est vrai si la texture est vue de face, sans angle, mais ne l'est pas si la texture n'est pas perpendiculaire à l'axe de la caméra. Du point de vue de la caméra, les 4 texels forment un trapèze d'autant moins proche d'un carré que l'angle est grand.
Pour corriger cela, les chercheurs ont inventé le '''filtrage anisotrope'''. En fait, je devrais plutôt dire : LES filtrages anisotropes. Il en existe un grand nombre, dont certains ne sont pas utilisés dans les cartes graphiques actuelles, soit car ils trop gourmand en accès mémoires et en calculs pour être efficaces, soit car ils ne sont pas pratiques à mettre en œuvre. Il est très difficile de savoir quelles sont les techniques de filtrage de texture utilisées par les cartes graphiques, qu'elles soient récentes ou anciennes. Beaucoup de ces technologies sont brevetées ou gardées secrètes, et il faudrait vraiment creuser les brevets déposés par les fabricants de GPU pour en savoir plus. Les algorithmes en question seraient de plus difficiles à comprendre, les méthodes mathématiques cachées derrière ces méthodes de filtrage n'étant pas des plus simple.
[[File:Anisotropic filtering en.png|centre|vignette|upright=2|Exemple de filtrage anisotrope.]]
==La compression de textures==
Les textures les plus grosses peuvent aller jusqu'au mébioctet, ce qui est beaucoup. Pour limiter la casse, les textures sont compressées. La '''compression de texture''' réduit la taille des textures, ce qui peut se faire avec ou sans perte de qualité. Elle entraîne souvent une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande. Mais il s'agit là d'une technique très simple, beaucoup plus simple que les techniques que nous allons voir dans cette section. Nous allons voir quelque algorithmes de compression de textures de complexité intermédiaire, mais n'allons pas voir l'état de l'art. Il existe des formats de texture plus récents que ceux qui nous allons aborder, comme l{{'}}''Ericsson Texture Compression'' ou l{{'}}''Adaptive Scalable Texture Compression'', plus complexes et plus efficaces.
Notons que les textures sont compressées dans les fichiers du jeu, mais aussi en mémoire vidéo. Les textures sont décompressées lors de la lecture. Pour cela, la carte graphique contient alors un circuit, capable de décompresser les textures lorsqu'on les lit en mémoire vidéo. Les cartes graphiques supportent un grand nombre de formats de textures, au niveau du circuit de décompression. Du fait que les textures sont décompressées à la volée, les techniques de compression utilisées sont assez particulières. La carte graphique ne peut pas décompresser une texture entière avant de pouvoir l'utiliser dans un ''pixel shader''. A la place, on doit pouvoir lire un morceau de texture, et le décompresser à la volée. On ne peut utiliser les méthodes de compression du JPEG, ou d'autres formats de compression d'image. Ces dernières ne permettent pas de décompresser une image morceau par morceau.
Pour permettre une décompression/compression à la volée, les textures sont des textures tilées, généralement découpées en tiles de 4 * 4 texels. Les ''tiles'' sont compressées indépendamment les unes des autres. Et surtout, avec ou sans compression, la position des tiles en mémoire ne change pas. On trouve toujours une tile tous les T octets, peu importe que la tile soit compressée ou non. Par contre, une tile compressée n'occupera pas T octets, mais moins, là où une tile compressée occupera la totalité des T octets. En clair, compresser une tile fait qu'il y a des vides entre deux tiles dans al mémoire vidéo, mais ne change rien à leur place en mémoire vidéo qui est prédéterminée, peu importe que la texture soit compressée ou non. L'intérêt de la compression de textures n'est pas de réduire la taille de la texture en mémoire vidéo, mais de réduire la quantité de données à lire/écrire en mémoire vidéo. Au lieu de lire T octets pour une tile non-compressée, on pourra en lire moins.
===La palette indicée et la technique de ''Vector quantization''===
La technique de compression des textures la plus simple est celle de la '''palette indicée''', que l'on a entraperçue dans le chapitre sur les cartes d'affichage. La technique de '''''vector quantization''''' peut être vue comme une amélioration de la palette, qui travaille non pas sur des texels, mais sur des ''tiles''. À l'intérieur de la carte graphique, on trouve une table qui stocke toutes les ''tiles'' possibles. Chaque ''tile'' se voit attribuer un numéro, et la texture sera composé d'une suite de ces numéros. Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l’embarqué utilisent ce genre de compression.
===Les algorithmes de ''Block Truncation coding''===
La première technique de compression élaborée est celle du '''''Block Truncation Coding''''', qui ne marche que pour les images en niveaux de gris. Le BTC ne mémorise que deux niveaux de gris par ''tile'', que nous appellerons couleur 1 et couleur 2, les deux niveaux de gris n'étant pas le même d'une ''tile'' à l'autre. Chaque pixel d'une ''tile'' est obligatoirement colorié avec un de ces niveaux de gris. Pour chaque pixel d'une ''tile'', on mémorise sa couleur avec un bit : 0 pour couleur 1, et 1 pour couleur 2. Chaque ''tile'' est donc codée par deux entiers, qui codent chacun un niveau de gris, et une suite de bits pour les pixels proprement dit. Le circuit de décompression est alors vraiment très simple, comme illustré ci-dessous.
[[File:Block Truncation coding.jpg|centre|vignette|upright=2.0|Block Truncation coding.]]
La technique du BTC peut être appliquée non pas du des niveaux de gris, mais pour chaque composante Rouge, Vert et Bleu. Dans ces conditions, chaque ''tile'' est séparée en trois sous-''tiles'' : un sous-bloc pour la composante verte, un autre pour le rouge, et un dernier pour le bleu. Cela prend donc trois fois plus de place en mémoire que le BTC pur, mais cela permet de gérer les images couleur.
===Le format de compression S3TC / DXTC===
L'algorithme de '''Color Cell Compression''', ou CCC, améliore le BTC pour qu'il gère des couleurs autre que des niveaux de gris. Ce CCC remplace les deux niveaux de gris par deux couleurs. Une ''tile'' est donc codée avec un entier 32 bits par couleur, et une suite de bits pour les pixels. Le circuit de décompression est identique à celui utilisé pour le BTC.
[[File:Color Cell Compression.jpg|centre|vignette|Color Cell Compression.]]
[[File:Dxt1-memory-layout.png|vignette|Dxt1 et ''color cell compression''.]]
Le format de compression de texture utilisé de base par Direct X, le DXTC, est une version amliorée de l'algorithme précédent. Il est décliné en plusieurs versions : DXTC1, DXTC2, etc. La première version du DXTC est une sorte d'amélioration du CCC : il ajoute une gestion minimale de transparence, et découpe la texture à compresser en ''tiles'' de 4 pixels de côté. La différence, c'est que la couleur finale d'un texel est un mélange des deux couleurs attribuée au bloc. Pour indiquer comment faire ce mélange, on trouve deux bits de contrôle par texel.
Si jamais la couleur 1 < couleur2, ces deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (2 * Couleur1 + Couleur2) / 3
* 11 = (Couleur1 + 2 * Couleur2) / 3
Sinon, les deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (Couleur1 + Couleur2) / 2
* 11 = Transparent
[[File:DXTC.jpg|centre|vignette|DXTC.]]
Le circuit de décompression du DXTC ressemble alors à ceci :
[[File:Circuit de décompression du DXTC.jpg|centre|vignette|upright=2.0|Circuit de décompression du DXTC.]]
===Les format DXTC 2, 3, 4 et 5 : l'ajout de la transparence===
Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3, lui-même replacé par le DXT4 et par le DXT5. Dans le DXT3, la transparence fait son apparition. Pour cela, on ajoute 64 bits par ''tile'' pour stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1.
[[File:Dxt23-memory-layout.png|centre|vignette|Dxt 2 et 3.]]
Dans le DXT4 et le DXT5, la méthode utilisée pour compresser les couleurs l'est aussi pour les valeurs de transparence. L'information de transparence est stockée par un en-tête contenant deux valeurs de transparence, le tout suivi d'une matrice qui attribue trois bits à chaque texel. En fonction de la valeur des trois bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1.
[[File:Dxt45-memory-layout.png|centre|vignette|Dxt 4 et 5.]]
===Le format de compression PVRTC===
Passons maintenant à un format de compression de texture un peu moins connu, mais pourtant omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle travaille surtout dans les cartes graphiques embarquées. Ses cartes se trouvent notamment dans l'ipad, l'iPhone, et bien d'autres smartphones actuels.
Avec le PVRTC, les textures sont encore une fois découpées en ''tiles'' de 4 texels par 4, mais la ressemblance avec le DXTC s’arrête là. Chacque ''tile'' est codée avec :
* une couleur codée sur 16 bits ;
* une couleur codée sur 15 bits ;
* 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
* et un bit de modulation, qui permet de configurer l’interprétation des bits de mélange.
Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4*4.
==Annexe : le ''normal-mapping'' hardware==
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, parlons un peu du ''normal mapping'' et de son implémentation dans les unités de texture. Pour rappel, 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. Et pour comprendre quel est le rapport avec les textures, nous allons devoir faire quelques rappels sur l'éclairage par pixel.
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
L'éclairage se fait en utilisant de nombreux calculs, qu'on a détaillé dans le chapitre sur les bases du rendu 3D. Ceux-ci utilisent la normale d'un sommet, à savoir un vecteur orienté à la verticale de la surface. En théorie, il y a une normale par sommet, ce qui fait que les calculs d'éclairage doivent se faire au niveau géométrique, avant la rastérisation. Mais pour obtenir un éclairage de meilleure qualité, il y a des techniques qui calcule l'éclairage d'une scène 3D pixel par pixel.
Une première technique d'éclairage par pixel interpole les normales lors de l'étape de rastérisation. On obtient alors un éclairage de Phong. Une autre solution est celle du ''normal mapping''. Le '''''normal mapping''''' précalcule les normales d'une surface dans une texture, appelée la ''normal map''. L'éclairage est alors réalisé par un pixel shader, qui lit les normales depuis cette texture et fait les calculs d'éclairage avec.
===Les ''normal maps'' sont des textures===
Notons que l'accès à la ''normal map'' se fait comme pour n'importe quelle texture : les texels sont lus en mémoire vidéo, puis le filtrage de texture est appliqué, et enfin le tout est envoyé au ''pixel shader''. En théorie, le filtrage effectue une sorte d'interpolation des normales automatique, ce qui permet de trouver la normale à un pixel précis, même s'il n'est pas sur un texel.
La ''normal map'' est une texture, donc. Mais ce n'est pas une texture comme une autre. Elle mémorise un vecteur pour chaque texel, pas une couleur. Il est possible d'utiliser les formats de textures vus plus haut, même si cela ne donne pas de bons résultats. Après tout, un vecteur est codé par trois coordonnées x,y,z, il est possible de coder chacune avec un octet et de packager cela dans une texture RGB classique. La coordonnée x va dans la composante Rouge, la coordonnée Y dans la composante Bleu, et la coordonnée z dans la composante z.
Le problème est que le rendu final n'est pas très beau. Et les opérations de filtrage ne donnent pas de très bons résultats. Pour remédier à ces problèmes, des formats de texture spécialisés pour les ''normal map'' ont été inventés. Le premier d'entre eux est le '''3Dc''', aussi appelé BC5 pour ''Block Compression 5''. Il utilise un octet par texel, au lieu de trois avec une texture RGB normale non-compressée.
===Le format de texture 3Dc===
Avec le 3Dc, les trois coordonnées des vecteurs ne sont pas mémorisées, seules deux d'entre elles le sont. En effet, les normales sont des vecteurs ''normalisés'', c'est à dire que leur longueur vaut 1, par construction. Vu que la taille est connue à l'avance, on peut en déduire la coordonnée z à partir des coordonnées x et y, avec l'usage du théorème de Pythagore. Par définition, <math>z^2 = 1 - x^2 - y^2</math>. Le calcul peut être fait dans le ''pixel shader'' sans problème.
: Pour être précis, il faut que les normales soient définies d'une certaine manière pour que ça marche. Les normales ne sont pas définies dans le même système de coordonnées que le modèle 3D, mais dans un autre système appelé l'''espace tangent''.
Pour le reste, la compression de la ''normal map'' ressemble un peu à celle des textures. La ''normal map'' est découpée en blocs de 4 par 4 texels de côté, chacune étant encodée en tenant compte de certaines redondances. Dans un bloc, les coordonnées x sont dans un certain intervalle, allant de <math>x_{min}</math> à <math>x_{max}</math>, idem pour la coordonnée y qui est dans l'intervalle <math>y_{min}</math>, <math>y_{max}</math>. L'intervalle <math>x_{min}</math> à <math>x_{max}</math> est coupé en 8 parts égales, ce qui fait 8 valeurs possibles dans cet intervalle, chacune correspondant à une valeur x possible pour une normale du bloc. La normale est donc encodée en précisant quelle valeur est la bonne, en utilisant un indice de 3 bits pour cela. La valeur exacte de la coordonnée x se calcule à partir de l'indice comme suit :
: <math>x = x_{min} + \left( \frac{x_{min} - x_{min}}{8} \right) \times \text{indice} </math>
Pour encoder un bloc de 4 par 4 normales, il faut donc :
* Un octet pour chaque coordonnée <math>x_{min}</math>, <math>x_{max}</math>, <math>y_{min}</math>, <math>y_{max}</math>.
* 6 bits par normale : 3 pour encoder le décalage de la coordonnée x, 3 pour la coordonnée y.
Le tout permet d'encoder un bloc de 16 normales en seulement 128 bits, soit un octet par normale. Le ''pixel shader'' se débrouille pour décompresser un bloc. Il effectue notamment le calcul du dessus, pour retrouver la coordonnée x. Les calculs étant particulièrement simples, le cout en performance est très faible. Et ce d'autant plus qu'ils demandent juste de faire des additions et des multiplications entières, que l'unité scalaire peut faire en parallèle d'autres opérations plus gourmandes. Et le cout en calculs est de toute façon compensé par l'économie de mémoire et de bande passante lié à la compression : diviser la taille des données par trois, ca a un sacré impact !
==Annexe : les ''shadowmap'' hardware==
Les anciens GPU, notamment la Geforce FX, avaient des fonctionnalités spécifiques pour le calcul des ombres. Dans la plupart des jeux vidéos de l'époque, et même de maintenant, les ombres sont calculées avec la technique des ''shadowmap''. L'idée est assez simple sur le principe : un pixel est dans l'ombre quand il est invisible depuis une source de lumière.
L'idée est que le rendu est réalisé en plusieurs passes, avec une passe par source de lumière et une passe finale pour calculer l'image finale. Nous allons expliquer la technique avec une seule source de lumière, et allons utiliser l'exemple de la scène ci-dessous.
[[File:7fin.png|centre|vignette|Scène 3D d'exemple.]]
===La technique du ''shadowmapping''===
[[File:2shadowmap.png|vignette|Résultat de la première passe : ''shadowmap''..]]
La première passe rend l'image depuis le point de vue de la source de lumière. Cette première passe ne rend pas les couleurs de la scène, elle ne s'intéresse qu'à la profondeur des pixels. Le résultat est que l'image ne rend que le tampon de profondeur. Celui-ci est ensuite réutilisé comme texture pour la passe suivante. La texture en question est appelée la '''''shadownmap'''''.
La perspective utilisée, ainsi que le ''view frustrum'', dépend de la source de lumière. Pour une source de lumière qui émet un cône de lumière, le ''view frustrum'' de l'image rendue doit contenir tout le cone de lumière, et doit coller le plus possible à celui-ci. Pour une source directionnelle, comme le soleil, une perspective orthographique est utilisée.
La seconde passe rend l'image du point de vue de la caméra, pour rendre l'image finale. Elle rend l'image finale, qui est composée de pixels, chacun ayant une position à l'écran x,y, et une profondeur z. Les coordonnées sont transformées pour obtenir la position de ce pixel depuis le point de vue de la caméra. Une simple multiplication de matrice suffit, rien de bien compliqué, un shader peut le faire.
[[File:5failed.png|vignette|Résultat du test des comparaisons.]]
Après cette étape, on a alors les coordonnées x,y,z de ce pixel du point de vue de la caméra, et la ''shadowmap''. Il est alors possible d'accéder à la ''shadowmap'' au même endroit, à la même place que le pixel testé, aux mêmes coordonnées x,y. Si la profondeur du pixel est supérieure à celle de la shadowmap au même endroit, alors le pixel est situé derrière la surface visible, donc est dans l'ombre. Sinon, il n'est pas dans l'ombre. Le même procédé est répété sur chaque pixel de l'écran.
===Les optimisations hardware du ''shadowmapping''===
La technique des ''shadowmap'' demande donc de calculer une texture ''shadowmap'', puis de lire celle-ci et de faire des comparaisons de profondeur. Les GPU comme la Geforce FX intégraient du matériel dans les unités de texture pour faciliter ce travail. Les unités de texture pouvaient lire les ''shadowmap'', et faire la comparaison de profondeur toutes seules, elles avaient des circuits pour. Il suffisait de leur fournir le pixel à tester, ses coordonnées x,y,z, et l'adresse de la ''shadowmap''. Les unités de texture renvoyaient alors un résultat valant 0 ou 1 : 1 si le pixel est dans l'ombre, 0 sinon.
Elles pouvaient même effectuer du filtrage de texture sur les ''shadowmap''. Mais le filtrage était différent de celui utilisé sur les autres textures : moyenner des valeurs de profondeur ne marche pas bien. Elles utilisaient des techniques de filtrage différentes : elles faisaient les tests de comparaison, puis faisaient la moyenne des résultats. Ainsi, pour du filtrage bilinéaire, elles lisaient 4 texels dans la ''shadowmap'', puis faisaient 4 tests de comparaison, et moyennaient les 4 résultats.
==Annexe : les textures virtuelles==
Les '''textures virtuelles''' sont une optimisation des textures normales, qui visent à accélérer le rendu de terrains de grande taille. Imaginez par exemple un monde assez ouvert, comme un environnement en forêt ou en montagne, avec une grande distance de visibilité. Avec de tels terrains, le "sol" est recouvert par une texture de sol unique qui recouvre tout le terrain. Elle ne se répète pas, est de très grande taille, et peut parfois recouvrir toute la map ! Mais il n'y a pas assez de mémoire vidéo pour mémoriser la texture toute entière. La seule solution est la suivante : une partie de la texture est placée en mémoire vidéo, le reste est soit placé en mémoire RAM ou sur le disque dur.
Pour cela, le moteur de jeu utilise une optimisation ingénieuse, basée sur une observation assez basique : une bonne partie de la texture est visible, mais le reste est caché par des arbres, des habitations ou d'autres obstacles. Une optimisation possible de ne garder en mémoire vidéo que les portions visibles de la texture, pas les portions cachées. Une autre optimisation mélange textures virtuelles et ''mip-mapping''. L'idée est que pour les portions lointaines d'une texture, la texture utilisée est une ''mip-map'' de basse résolution. L'idée est alors de ne charger que la ''mip-map'' adéquate, pas les autres niveaux de détail. En clair, la texture de base n'est pas chargée en mémoire vidéo, mais la ''mip-map'' basse résolution l'est.
===Une texture à deux niveaux===
L'implémentation des textures virtuelles découpe les méga-textures en ''tiles'', en morceaux rectangulaires de taille modeste. En clair, le terrain est découpé en morceau rectangulaires/carrés. Seules les tiles nécessaires sont chargées en mémoire vidéo, pas les autres. Par exemple, les ''tiles'' non-visibles ne sont pas placées en mémoire vidéo, seules les ''tiles'' visibles le sont. De même, il y a une ''tile'' par niveau de mip-map : seul la tile correspondant le niveau adéquat est en mémoire vidéo, les autres niveaux de détail ne sont pas chargés. On peut faire une analogie avec la mémoire virtuelle, où les données sont découpées en pages, qui sont chargées en mémoire RAM à la demande, suivant les besoins, les données pouvant être swappées sur le disque dur si elles sont peu utilisées. Sauf qu'ici, il s'agit de textures qui sont découpées en pages chargées à la demande en mémoire vidéo, depuis la RAM système.
Une texture virtuelle est en réalité un système à deux niveaux : une liste de ''tiles'' et les ''tiles'' elles-mêmes. La liste de ''tiles'' est appelée un '''atlas de texture''', c'est un peu l'équivalent de la ''tilemap'' pour le rendu 2D. Rendre une texture demande de calculer quelle ''tile'' contient le texel à afficher, consulter la ''tile'' en question, puis récupérer le texel adéquat dans cette ''tile''. La ''tile'' est donc une texture, mais la texture à charger est choisie parmi un ensemble, qui est ici l'atlas de texture.
===L'implémentation : logicielle versus matérielle===
Les textures virtuelles ont été utilisées pour la première fois par les jeux Rage 1 et 2 d'IdSoftware, et quelques jeux ultérieurs comme DOOM 2016. IdSoftware les appelait des '''''mega-textures'''''. L'optimisation permettait des gains en performance assez impressionnants. Le jeu Rage 1 utilisait une texture carrée unique de 128k pixels de côté pour rendre le terrain. En théorie, une telle texture devrait prendre 64 giga-octets, mais le jeu tournait correctement avec 512 méga-octets de RAM, poussivement avec seulement 256 méga-octets de RAM.
De nos jours, les textures virtuelles sont supportées par beaucoup de jeux vidéos, les moteurs les plus courants gèrent de telles textures de manière logicielles. Mais quelques GPU récents supportent les textures virtuelles. Sur les GPU récents, l'atlas de texture est géré nativement par le matériel. Le GPU choisit quelle ''tile'', quelle texture choisir pour rendre le texel adéquat. Pour cela, le GPU calcule quelle ''tile'' charger, consulte l'atlas de texture, et lit la texture de ''tile'' adéquate.
Mais l'implémentation sur les GPU récents a de nombreuses limitations. La limitation la plus importante est que la taille des textures virtuelles ne peut pas dépasser la taille d'une texture normale, soit 32768 pixels de côté pour une texture carrée environ sur les GPU de 2020. De plus, le chargement d'une ''tile'' est très lent. En clair, dès qu'on veut changer de niveau de mip-map pour une tile, ou dès qu'une tile devient visible, le chargement de la tile peut facilement prendre plusieurs centaines de millisecondes. Le filtrage de texture est très complexe avec des textures virtuelles, ce qui fait que le filtrage de texture virtuelle est souvent soumis à des limitations que les textures normales n'ont pas, notamment pour le filtrage anisotropique.
{{NavChapitre | book=Les cartes graphiques
| prev=Le rasterizeur
| prevText=Le rasterizeur
| next=Les Render Output Target
| nextText=Les Render Output Target
}}{{autocat}}
glr2v8wjn0qg89xkfomk9wtmej223u4
763415
763413
2026-04-10T18:28:53Z
Mewtow
31375
/* Annexe : le normal-mapping hardware */
763415
wikitext
text/x-wiki
[[File:Texture mapping.png|vignette|''Texture mapping'']]
Les '''textures''' sont des images que l'on va plaquer sur la surface d'un objet, du papier peint en quelque sorte. Les cartes graphiques supportent divers formats de textures, qui indiquent comment les pixels de l'image sont stockés en mémoire : RGB, RGBA, niveaux de gris, etc. Une texture est donc composée de "pixels", comme toute image numérique. Pour bien faire la différence entre les pixels d'une texture, et les pixels de l'écran, les pixels d'une texture sont couramment appelés des ''texels''.
==Le placage de textures inverse==
Pour rappel, plaquer une texture sur un objet consiste à attribuer un texel à chaque sommet, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture.
Dans les faits, on n'utilise pas de coordonnées entières de ce type. Les coordonnées de texture sont deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. Le nom donnée à cette technique de description des coordonnées de texture s'appelle l''''''UV Mapping'''''.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Les API 3D modernes gèrent des textures en trois dimensions, ce qui ajoute une troisième coordonnée de texture notée w. Dans ce qui va suivre, nous allons passer les textures en trois dimensions sous silence. Elles ne sont pas très utilisées, la quasi-totalité des jeux vidéo et applications 3D utilisant des textures en deux dimensions. Par contre, le matériel doit gérer les textures 3D, ce qui le rend plus complexe que prévu. Il faut ajouter quelques circuits pour, de quoi gérer la troisième coordonnée de texture, etc.
Lors de la rastérisation, chaque fragment se voit attribuer un sommet, et donc la coordonnée de texture qui va avec. Si un pixel est situé pile sur un sommet, la coordonnée de texture de ce sommet est attribuée au pixel. Si ce n'est pas le cas, la coordonnée de texture finale est interpolée à partir des coordonnées des trois sommets du triangle rastérisé. L'interpolation en question a lieu dans l'étape de rastérisation, comme nous l'avons vu dans le chapitre précédent. Le fait qu'il y ait une interpolation fait que les coordonnées du pixel gagent à être des nombres flottants. On pourrait faire une interpolation avec des coordonnées de texture entières, mais les arrondis et autres imprécisions de calcul donneraient un résultat graphiquement pas terrible, et empêcheraient d'utiliser les techniques de filtrage de texture que nous verrons dans ce chapitre.
À partir de ces coordonnées de texture, la carte graphique calcule l'adresse du texel qui correspond, et se charge de le lire. Et toute la magie a lieu dans ce calcul d'adresse, qui part de coordonnées de texture flottante, pour arriver à une adresse mémoire. Le calcul de l'adresse du texel se fait en plusieurs étapes, que nous allons voir ci-dessous. La première étape convertit les coordonnées flottantes en coordonnées entières, qui disent à quel ligne et colonne se trouve le texel voulu dans la texture. L'étape suivante transforme ces coordonnées x,y entières en adresse mémoire.
===La normalisation des coordonnées===
J'ai dit plus haut que les coordonnées de texture sont des coordonnées flottantes, comprises entre 0 et 1. Mais il faut savoir que les pixels shaders peuvent modifier celles-ci pour mettre en œuvre certains effets graphiques. Et le résultat peut alors se retrouver en-dehors de l'intervalle 0,1. C'est quelque chose de voulu et qui est traité par la carte graphique automatiquement, sans que ce soit une erreur. Au contraire, la manière dont la carte graphique traite cette situation permet d'implémenter des effets graphiques comme des textures en damier ou en miroir.
[[File:Clamp tile.jpg|vignette|Clamp tile]]
Il existe globalement trois méthodes très simples pour gérer cette situation, qui sont appelés des '''modes d'adressage de texture'''.
* La première méthode est de faire en sorte que le résultat sature. Si une coordonnée est inférieur à 0, alors on la remplace par un zéro. Si elle est supérieure à 1, on la ramène à 1. Avec cette méthode, tout se passe comme si les bords de la texture étaient étendus et remplissaient tout l'espace autour de la texture. Le tout est illustré ci-dessous. Ce mode d'accès aux textures est appelé le '''''clamp'''''.
* Une autre solution retire la partie entière de la coordonnée, elle coupe tout ce qui dépasse 1. Pour le dire autrement, elle calcule le résultat modulo 1 de la coordonnée. Le résultat est que tout se passe comme si la texture était répétée à l'infini et qu'elle pavait le plan.
* Une autre méthode remplit les coordonnées qui sortent de l’intervalle 0,1 avec une couleur préétablie, configurée par le programmeur.
===La conversion des coordonnées de textures flottantes en adresse mémoire===
Une fois la normalisation effectuée, les coordonnées de texture sont utilisées pour lire le texel voulu. Pour cela, les coordonnées de texte sont transformées en adresse mémoire, adresse qui pointe sur le texel ayant ces cordonnées. Pour cela, la première étape est de transformer les coordonnées flottantes u,v en coordonnées entières x,y qui pointent sur un texel. Pour cela, il suffit de multiplier les coordonnées flottantes u,v par la résolution de la texture accédée. Pour un écran de résolution <math>\text{height,width}</math>, le calcul est le suivant :
: <math>x = u \times \text{width}</math>
: <math>y = v \times \text{height}</math>
Le résultat est un nombre avec une partie entière et une partie fractionnaire. La partie entière des deux coordonnées donne la position x,y voulue, et la partie fractionnaire est conservée pour le filtrage de textures, mais passons cela sous silence pour le moment.
La seconde étape prend les coordonnées entières x,y et calcule l'adresse mémoire du texel. L'adresse dépend de la position de la texture en mémoire, précisément de son début, son premier texel, mais aussi de la position du texel par rapport au début de la texture. Et calculer cette position intra-texture dépend de la manière dont les texels sont stockés en mémoire.
====Les textures naïves====
Les programmeurs qui lisent ce cours s'attendent certainement à ce que la texture soit stockée en mémoire ligne par ligne, ou colonne par colonne. Cela veut dire que le premier pixel en partant d'en haut à gauche est stocké en premier, puis celui immédiatement à sa droite, puis celui encore à droite, et ainsi de suite. Une fois qu'on arrive à la fin d'une ligne, on passe à la ligne suivante, en-dessous. Cette organisation ligne par ligne s'appele l'organisation '''''row major order'''''. On peut faire pareil, mais colonne par colonne, ce qui donne le '''''column major order'''''.
[[File:Speicheranordnung Feld.svg|centre|vignette|upright=2|Row et column major order.]]
Maintenant, supposons que la texture commence à l'adresse <math>A_\text{texture}</math>, qui est l'adresse du premier texel. La texture a une résolution de <math>\text{width}</math> texels de large et <math>\text{height}</math> texels de haut. Par définition, les coordonnées X et Y des texels commencent à 0, ce qui fait que le pixel en haut à gauche a les coordonnées 0,0.
L'adresse du pixel se calcule comme suit :
: <math>A_\text{pixel} = A_\text{texture} + (\text{taille d'une ligne en octets} \times Y) + (\text{taille d'un texel en octets} \times X)</math>
La taille d'un pixel en mémoire est notée T. La taille d'une ligne en mémoire est de <math>width \times T</math>, par définition, vu qu'elle fait <math>width</math> texels. On a donc :
: <math>A_\text{pixel} = A_\text{texture} + (width \times T \times Y) + (T \times X)</math>
La formule se réécrit comme suit :
: <math>A_\text{pixel} = A_\text{texture} + T \times (width \times Y + X)</math>
Le calcul d'adresse est donc assez simple. Malheureusement, les textures ne sont pas stockées de cette manière en mémoire vidéo. En effet, elle se marie mal avec les opérations de filtrage de texture que nous allons voir dans ce qui suit. Le filtrage d'un texel dépend de ses voisins du dessus et du dessous. Le fait que la texture n'est pas forcément parcourue ligne par ligne fait que stocker une texture ligne par ligne n'est pas l'idéal.
De même, les textures sont déformées par la perspective. L'affichage de la texture ne se fait alors pas ligne par ligne, mais en parcourant la texture en diagonale, l'angle de la diagonale correspondant approximativement à l'angle que fait la verticale de la texture avec le regard. Vu qu'on ne connait pas à l'avance l'angle que fera la diagonale de parcours, on doit ruser.
====Les textures tilées====
Une première solution à ce problème est celle des '''textures tilées'''. Avec ces textures, l'image de la texture est découpée en ''tiles'', des rectangles ou en carrés de taille fixe, généralement des carrés de 4 pixels de côté. Les tiles ont une largeur et une longueur égales, afin de simplifier les calculs : on divise X et Y par le même nombre. De plus, leur largeur et leur longueur sont une puissance de deux, afin de simplifier les calculs d'adresse. Les ''tiles'' sont alors mémorisée les unes après les autres dans le fichier de la texture.
[[File:Texture tilée.png|centre|vignette|upright=2|Texture tilée]]
La formule de calcul d'adresse vue plus haut doit être adaptée pour tenir compte des tiles. Pour cela, il faut remplacer la taille d'un texel par la taille d'une tile, et que la largeur de la texture soit exprimée en nombre de tiles. De plus, on doit adapter les coordonnées des texels pour donner des coordonnées de tile. Généralement, les tiles sont des carrés de N pixels de côté, ce qui fait qu'on peut regrouper les lignes et les colonnes par paquets de N. Il suffit donc de diviser Y et X pour obtenir les coordonnées de la tile, de même que la larguer. La formule pour calculer la position de la énième tile est alors la suivante :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + \text{Taille mémoire d'une tile} \times \left( {\text{Width} \over N} \times {Y \over N} + {X \over N} \right)</math>
On peut réécrire le tout comme suit :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + K \times \left( {Y \over N} + X \right)</math>, avec K une constante connue à la compilation des shaders.
Vu que les tiles sont carrées avec une largeur qui est une puissance de deux, la multiplication par la taille d'une tile en mémoire se simplifie : on passe d'une multiplication entière à des décalages de bits. Même chose pour le calcul de l'adresse de la tile à partir des coordonnées x,y : ils impliquent des divisions par une puissance de deux, qui deviennent de simples décalages.
La position d'un pixel dans une tile dépend du format de la texture, mais peut se calculer avec quelques calculs arithmétiques simples. Dans les cas les plus simples, les pixels sont mémorisés ligne par ligne, ou colonne par colonne. Mais ce n'est pas systématiquement le cas. Toujours est-il que les calculs pour déterminer l'adresse sont simples, et ne demandent que quelques additions ou multiplications. Mais avec les formats de texture utilisés actuellement, les tiles sont chargées en entier dans le cache de texture, sans compter que diverses techniques de compression viennent mettre le bazar, comme on le verra dans la suite de cours.
Un avantage de l'organisation en tiles est qu'elle se marie bien avec le parcours des textures. On peut parcourir une texture dans tous les sens, horizontal, vertical, ou diagonal, on sait que les prochains pixels ont de fortes chances d'être dans la même tile. Si on rentre dans une tile par la gauche en haut, on a encore quelques pixels à parcourir dans la tile, par exemple. De même, le filtrage de textures est facilité. On verra dans ce qui va suivre que le filtrage de texture a besoin de lire des blocs de 4 texels, des carrés de 2 pixels de côté. Avec l'organisation en tile, on est certain que les 4 texels seront dans la même tile, sauf s'ils ont le malheur d'être tout au bord d'une tile. Ce dernier cas est assez rare, et il l'est d'autant plus que les tiles sont grandes. Enfin, un dernier avantage est que les tiles sont généralement assez petites pour tenir tout entier dans une ligne de cache. Le cache de texture est donc utilisé à merveille, ce qui rend les accès aux textures plus rapides.
====Les textures basées sur des ''z-order curves''====
Les formats de textures théoriquement optimaux utilisent une '''''Z-order curve''''', illustrée ci-dessous. L'idée est de découper la texture en quatre rectangles identiques, et de stocker ceux-ci les uns à la suite des autres. L'intérieur de ces rectangles est lui aussi découpé en quatre rectangles, et ainsi de suite. Au final, l'ordre des pixels en mémoire est celui illustré ci-dessous.
[[File:Z-CURVE.svg|centre|vignette|upright=2|Construction d'une ''Z-order curve''.]]
Les texels sont stockés les uns à la suite des autres dans la mémoire, en suivant l'ordre donnée par la ''Z-order curve''. Le calcul d'adresse calcule la position du texel en mémoire, par rapport au début de la texture, et ajoute l'adresse du début de la texture. Mais tout le défi est de calculer la position d'un texel en mémoire, à partir des coordonnées x,y. Le calcul peut sembler très compliqué, mais il n'en est rien. Le calcul demande juste de regarder les bits des deux coordonnées et de les combiner d'une manière particulièrement simple. Il suffit de placer le bit de poids fort de la coordonnée x, suivi de celui de la coordonnée y, et de faire ainsi de suite en passant aux bits suivants.
[[File:Zcurve45bits.png|centre|vignette|upright=1.5|Calcul de la position d'un élément dans une ''Z-order curve'' à partir des coordonnées x et y.]]
L'avantage d'une telle organisation est que la textures est découpées en ''tiles'' rectangulaires d'une certaine taille, elles-mêmes découpées en ''tiles'' plus petites, etc. Et il se trouve que cette organisation est parfaite pour le cache de texture. L'idéal pour le cache de texture est de charger une ''tile'' complète dans le cache de textures. Quand on accède à un texel, on s'assure que la ''tile'' complète soit chargée. Mais cela demande de connaitre à l'avance la taille d'une ''tile''. Les formats de texture fournissent généralement une ''tile'' carré de 4 pixels de côté, mais cela donnerait un cache trop petit pour être vraiment utile. Avec cette méthode, on s'assure qu'il y ait une ''tile'' avec la taille optimale. Les ''tiles'' étant découpées en ''tiles'' plus petites, elles-mêmes découpées, et ainsi de suite, on s'assure que la texture est découpées en ''tiles'' de taille variées. Il y aura au moins une ''tile'' qui rentrera tout pile dans le cache.
==Les techniques de rendu à textures multiples==
Nous venons de voir comment une texture est plaquée sur un objet 3D, ou une surface comme un sol. Pour résumer, le calcul de l'adresse d'un texel prend la position du texel par rapport au début de la texture, et ajoute l'adresse du début de la texture. L'adresse mémoire de la texture est connue au moment où le pilote de la carte graphique place la texture dans la mémoire vidéo, et cette information est transmise au matériel par l'intermédiaire du processeur de commande, puis passée aux processeurs de shaders et à l'unité de texture. Le tout est couplé à d'autres informations, la plus importante étant la ''taille de la texture en octets'', pour éviter de déborder lors des accès à la texture.
Néanmoins, il s'agit là du cas le plus simple. Certaines techniques de rendu demandent de choisir la texture à plaquer parmi un ensemble de plusieurs textures. Les techniques en question sont assez variées et n'ont pas grand chose en commun. Les plus connues sont le ''mip-mapping'', le ''cube-mapping'' et les textures virtuelles. Le ''mip-mapping'' sert à filtrer les textures, chose qu'on expliquera plus tard, le ''cube-mapping'' sert à simuler des réflexions sur un objet en plaquant une texture de l'environnement dessus, les textures virtuelles sont une optimisation pour les textures des terrains de grande taille. Mais malgré leurs différences, elles demandent de choisir quelle texture plaquer entre plusieurs textures de base. En clair, l'adresse de base de la texture varie selon la situation. Voyons-les dans le détail.
===Le mip-mapping===
Le '''mip-mapping''' a pour but de légèrement améliorer les graphismes des objets lointains, tout en rendant les calculs de texture plus rapides. Formellement, le ''mip-mapping'' est une technique de filtrage de texture, mais nous l'abordons maintenant car elle est surtout liée au calcul d'adresse. Les unités de texture ont des circuits de filtrage de texture séparés des circuits de ''mip-mapping'' et de calcul d'adresse, d'où le fait que nous en parlons séparément.
Le problème résolu par le ''mip-mapping'' est le rendu des textures lointaines. Si une texture est plaquée sur un objet lointain, une bonne partie des détails est invisible pour l'utilisateur. Un pixel de l'écran est associé à plusieurs texels. Idéalement, la carte graphiques devrait lire tous ces texels et en faire une sorte de moyenne pondérée, pour calculer la couleur finale du pixel. Mais dans les faits, ce serait très gourmand et compliqué à implémenter en hardware. Une solution serait de ne garder que quelque texels, mais cela a tendance à créer des artefacts visuels (les textures affichées ont tendance à pixeliser). Le ''mip-mapping'' permet de réduire ces deux problèmes en même temps en précalculant cette moyenne pondérée pour des distances prédéfinies.
L'idée est d'utiliser plusieurs exemplaires d'une même texture à des résolutions différentes, chaque exemplaire étant adapté à une certaine distance. Par exemple, une texture sera stocké avec un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre de 128 * 128 et ainsi de suite jusqu’à un dernier exemplaire de 32 * 32 pixel. Chaque exemplaire correspond à un '''niveau de détail''', aussi appelé ''Level Of Detail'' (abrévié en LOD). La résolution utilisée diminue d'autant plus que l'objet est situé loin de la caméra. Les objets proches seront rendus avec la texture 512*512, ceux plus lointains seront rendus avec la texture de résolution 256*256, les textures 128*128 seront utilisées encore plus loin, et ainsi de suite jusqu'aux objets les plus lointains qui sont rendus avec la texture la plus petite de 32*32.
[[File:MipMap Example STS101.jpg|centre|vignette|upright=2|Exemples de mip-maps.]]
Le ''mip-mapping'' améliore grandement la qualité d'image. L'image d'exemple ci-dessous le montre assez bien.
[[File:Mipmapping example.png|centre|vignette|upright=2|Exemple de mipmapping.]]
Pour faciliter les calculs d'adresse, les LOD d'une même texture sont stockées les uns après les autres en mémoire (dans un tableau, comme diraient les programmeurs). Ainsi, pas besoin de se souvenir de la position en mémoire de chaque LOD : l'adresse de la texture de base, et quelques astuces arithmétiques suffisent. Prenons le cas où la texture de base a une taille L. le premier exemplaire est à l'adresse 0, le second niveau de détail est à l'adresse L, le troisième à l'adresse L + L/4, le suivant à l'adresse L + L/4 + L/16, et ainsi de suite. Le calcul d'adresse demande juste connaître le niveau de détails souhaité et l'adresse de base de la texture. Le niveau de détail voulu est calculé par les pixel shaders, en fonction de la coordonnée de profondeur du pixel à traiter.
Évidemment, cette technique consomme de la mémoire vidéo, vu que chaque texture est dupliquée en plusieurs exemplaires, en plusieurs LOD. Dans le détail, la technique du mip-mapping prend au maximum 33% de mémoire en plus (sans compression). Cela vient du fait qu'en prenant une texture dexu fois plus petite, elle prend 4 fois moins de mémoire : 2 fois moins de pixels en largeur, et 2 fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des LODs, texture de base comprise, prendra X + (X/4) + (X/16) + (X/256) + … Un petit calcul de limite donne 4/3 * X, soit 33% de plus.
===Le cube-mapping===
[[File:Cube mapped reflection example 2.JPG|vignette|Exemple de reflets environnementaux.]]
L''''environnement-mapping''' est une technique de calcul de divers effets graphiques liés à l'environnement, notamment des réflexions. L'idée est de plaquer une texture pré-calculée pour simuler l'effet de l'environnement sur une surface ou un objet 3D. Il en existe plusieurs versions différentes, mais la seule utilisée de nos jours est le ''cube-mapping'', où la texture de l'environnement est plaquée sur un cube, d'où son nom. Le cube en question est utilisé différemment suivant ce que l'on cherche à faire avec le ''cube-mapping''. Les deux utilisations principales sont le rendu du ciel et des décors, et les réflexions sur la surface des objets. Dans les deux cas, l'idée est de précalculer ce que l'on voit du point de vue de la caméra. On place la caméra dans la scène 3D, on place un cube centré sur la caméra, le cube est texturé avec ce que l'on voit de l'environnement depuis la caméra/l'objet de son point de vue.
[[File:Panorama cube map.png|centre|vignette|upright=2|L'illustration montre en premier lieu une ''cubemap'' avec les six faces mises en évidence, puis quel environnement 3D elle permet de simuler, le troisième illustration montrant comment la ''cubemap'' est utilisée pour simuler l'environnement.]]
Le rendu du ciel et des décors lointains dans les jeux vidéo se base sur des '''''skybox''''', à savoir un cube centré sur la caméra, sur lequel on ajoute des textures de ciel ou de décors lointains. Le cube est recouvert par une texture, qui correspond à ce que l'on voit quand on dirige le regard de la caméra vers cette face. Contrairement à ce qu'on pourrait croire, la skybox n'est pas les limites de la scène 3D, les limites du niveau d'un jeu vidéo ou quoique ce soit d'autre de lié à la physique de la scène 3D. La skybox est centrée sur la caméra, elle suit la caméra dans son mouvement. Centrer la skybox sur la caméra permet de simuler des décors très lointains, suffisamment lointain pour qu'on n'ait pas l'illusion de s'en rapprocher en se déplaçant dans la map. De plus, cela évite d'avoir à faire trop de calculs à chaque fois que l'on bouge la caméra. La texture plaquée sur le cube est une texture unique, elle-même découpée en six sous-textures, une par face du cube.
[[File:Skybox example.png|centre|vignette|upright=2|Exemple de Skybox.]]
[[File:Cube mapped reflection example.jpg|vignette|Réflexions calculées par une ''cubemap''.]]
Le ''cube-mapping'' est aussi utilisé pour des reflets. L'idée est de simuler les reflets en plaquant une texture pré-calculée sur l'objet réflecteur. La texture pré-calculée est un dessin de l'environnement qui se reflète sur l'objet, un dessin du reflet à afficher. En la plaquant la texture sur l'objet, on simule ainsi des reflets de l'environnement, mais on ne peut pas calculer d'autres reflets comme les reflets objets mobiles comme les personnages. Et il se trouve que la texture pré-calculée est une ''cubemap''. Pour les environnements ouverts, c'est la ''skybox'' qui est utilisée, ce qui permet de simuler les reflets dans les flaques d'eau ou dans des lacs/océans/autres. Pour les environnements intérieurs, c'est une cubemap spécifique qui utilisée. Par exemple, pour l'intérieur d'une maison, on a une ''cubemap'' par pièce de la maison. Les reflets se calculent en précisant quelle ''cubemap'' appliquer sur l'objet en fonction de la direction du regard.
[[File:Cube map level.png|centre|vignette|Cube map de l'intérieur d'une pièce d'un niveau de jeux vidéo.]]
Toujours est-il que les textures utilisées pour le ''cubemmapping'', appelées des ''cubemaps'', sont en réalité la concaténation de six textures différentes. En mémoire vidéo, la ''cubemap'' est stockée comme six textures les unes à la suite des autres. Lors du rendu, on doit préciser quelle face du cube utiliser, ce qui fait 6 possibilités. On a le même problème qu'avec les niveaux de détail, sauf que ce sont les faces d'une ''cubemap'' qui remplacent les textures de niveaux de détails. L'accès en mémoire doit donc préciser quelle portion de la ''cubemap'' il faut accéder. Et l'accès mémoire se complexifie donc. Surtout que l'accès en question varie beaucoup suivant l'API graphique utilisée, et donc suivant la carte graphique.
Les API 3D assez anciennes ne gérent pas nativement les ''cubemaps'', qui doivent être émulées en logiciel en utilisant six textures différentes. Le pixel shader décide donc quelle ''cubemap'' utiliser, avec quelques calculs sur la direction du regard. L'accès se fait d'une manière assez simple : le shader choisit quelle texture utiliser. Les API 3D récentes gèrent nativement les ''cubemaps''. Dans le cas le plus simple,pour les versions les plus vielles de ces API, les six faces sont numérotées et l'accès à une ''cubemap'' précise quel face utiliser en donnant son numéro. La carte graphique choisit alors automatiquement la bonne texture, mais cela demande de laisser le calcul de la bonne face au pixel shader. D'autres API 3D et cartes graphiques font autrement. Dans les API 3D modenres, les ''cubemap'' sont gérées comme des textures en trois dimensions, adressées avec trois coordonnées u,v,w. La carte graphique utilise ces trois coordonnées de manière à en déduire quelle est la face pertinente, mais aussi les coordonnées u,v dans la texture de la face.
==L'implémentation matérielle du placage de textures==
Pour résumer, la lecture d'un texel demande d'effectuer plusieurs étapes. Dans le cas le plus simple, sans ''mip-mapping'' ou ''cubemapping'', on doit effectuer les étapes suivantes :
* Il faut d'abord normaliser les coordonnées de texture pour qu'elles tombent dans l'intervalle [0,1] en fonction du mode d'adressage désiré.
* Ensuite, les coordonnées u,v doivent être converties en coordonnées entières, ce qui demande une multiplication flottante.
* Enfin, l'adresse finale est calculée à partir des coordonnées entières et en ajoutant l'adresse de base de la texture (et éventuellement avec d'autres calculs arithmétiques suivant le format de la texture).
Tout cela pourrait être fait par le pixel shaders, mais cela implique beaucoup de calculs répétitifs et d'opérations arithmétiques assez lourdes, avec des multiplications flottantes, des additions et des multiplications entières, etc. Faire faire tous ces calculs par les shaders serait couteux en performance, sans compter que les shaders deviendraient plus gros et que cela aurait des conséquences sur le cache d'instruction. De plus, certaines de ces étapes peuvent se faire en parallèle, comme les deux premières, ce qui colle mal avec l'aspect sériel des shaders.
Aussi, les processeurs de shaders incorporent une unité de calcul d'adresse spéciale pour faire ces calculs directement en matériel. L'unité de texture contient au minimum deux circuits : un circuit de calcul d'adresse, et un circuit d'accès à la mémoire. Toute la difficulté tient dans le calcul d'adresse, plus que dans le circuit de lecture. Le calcul d'adresse est conceptuellement réalisé en deux étapes. La première étape qui transforme les coordonnées u,v en coordonnées x,y qui donne le numéro de la ligne et de la colonne du texel dans la texture. La seconde étape prend ces deux coordonnées x,y, l'adresse de la texture, et détermine l'adresse de la tile à lire.
[[File:Unité de texture simple.png|centre|vignette|upright=2|Unité de texture simple]]
===L'implémentation du mip-mapping===
Le ''mip-mapping'' est lui aussi pris en charge par l'unité de calcul d'adresse, car cette technique change l'adresse de base de la texture. La gestion du ''mip-mapping'' est cependant assez complexe. Il est possible de laisser le pixel shader calculer quel niveau de détail utiliser, en fonction de la coordonnée de profondeur z du pixel à afficher. La carte graphique détermine alors automatiquement quelle texture lire, quel niveau de détail, automatiquement. Elle détermine aussi la bonne résolution pour la texture, qui est égal à la résolution de la texture de base, divisée par le niveau de détail. Pour résumer, le niveau de détail est envoyé aux unités de texture, qui s'occupent de calculer l'adresse de base et la résolution adéquates. Quelques calculs arithmétiques simples, donc, qui s'implémentent facilement avec quelques circuits.
Mais une autre méthode laisse la carte graphique déterminer le niveau de détail par elle-même. Dans ce cas, cela demande, outre les deux coordonnées de texture, de calculer la dérivée de ces deux coordonnées dans le sens horizontal et vertical, ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Les quatre dérivées sont les suivantes :
: <math>\frac{du}{dx}</math>, <math>\frac{dv}{dx}</math>, <math>\frac{du}{dy}</math>, <math>\frac{dv}{dy}</math>
Un bon moyen pour obtenir les dérivées demande de regrouper les pixels par groupes de 4 et de faire la différence entre leurs coordonnées de texture respectives. On peut calculer les deux dérivées horizontales en comparant les deux pixels sur la même ligne, et les deux dérivées verticales en comparant les deux pixels sur la même colonne. Mais cela demande de rastériser les pixels par groupes de 4, par ''quads''. Et c'est ce qui est fait sur les cartes graphiques actuelles, qui rastérisent des groupes de 4 pixels à la fois.
[[File:Texture sampler unit with mipmapping.png|centre|vignette|upright=2.0|Unité de texture avec mipmapping.]]
Malheureusement, le calcul exact utilisé pour le choix de la mip-map dépend du GPU considéré et peu de chose est connu quant à ces algorithmes. Il est possible d'inférer le comportement à partir d'observations, mais guère plus. Pour ceux qui veulent en savoir plus, je conseille la lecture de cet article de blog :
* [https://pema.dev/2025/05/09/mipmaps-too-much-detail/ Mipmap selection in too much detail]
===La gestion des accès mémoire===
Enfin, l'unité de texture doit tenir compte du fait que la mémoire vidéo met du temps à lire une texture. En théorie, l'unité de texture ne devrait pas accepter de nouvelle demande de lecture tant que celle en cours n'est pas terminée. Mais faire ainsi demanderait de bloquer tout le pipeline, de l'''input assembler'' au unités de''shaders'', ce qui est tout sauf pratique et nuirait grandement aux performances.
Une solution alternative consiste à mettre en attente les demandes de lectures de texture pendant que la mémoire est occupée. La manière la plus simple d'implémenter des accès mémoire multiples est de les mettre en attente dans une petite mémoire FIFO. Cela implique que les accès mémoire s’exécutent dans l'ordre demandé par le ''shader'' et/ou l'unité de rastérisation, il n'y a pas de réorganisation des accès mémoire ou d’exécution dans le désordre des accès mémoire.
[[File:Texture prefetching.png|centre|vignette|upright=1.5|Accès mémoire simultanés.]]
Évidemment, quand la mémoire FIFO est pleine, le pipeline est alors totalement bloqué. Le rasteriser est prévenu que l'unité de texture ne peut pas accepter de nouvelle lecture de texture. En pratique, la FIFO est généralement d'une taille respectable et permet de mettre en attente beaucoup de demandes de lecture de texture. Il faut de plus noter qu'il y a une FIFO par processeur de ''shader'' sur les cartes graphiques modernes. Quand elle est pleine, le processeur cesse d'exécuter de nouveaux accès mémoire, mais peut continuer à exécuter des ''shaders'' dans les autres unités de calcul, pas besoin de bloquer complétement le pipeline.
===L'intégration du cache de textures===
Il faut noter que les unités de texture incorporent aussi un cache de texture, voire plusieurs. L'intégration des caches de texture avec la mémoire FIFO précédente est quelque peu compliqué, car il faut garantir que les lectures de texture se fassent dans le bon ordre. On ne peut pas exécuter une lecture dans le cache alors que des lectures précédentes sont en attente de lecture en mémoire vidéo. Et cela pose un gros problème : une lecture dans le cache de texture prend quelques dizaines de cycles d'horloge, alors qu'une lecture en mémoire vidéo en prend facilement 400 à 800 cycles, parfois plus. Et cela fait que l'ordre des accès mémoire peut s'inverser.
Prenons par exemple un accès au cache précédé et suivi par deux accès en mémoire vidéo. Le premier démarre au cycle 1, et se termine au cycle numéro 400. L'accès au cache commence au cycle 2 et se termine 20 cycles après, au cycle numéro 22. En clair, la lecture dans le cache s'est terminée avant l'accès mémoire qui le précède. Les textures ne sont donc plus lues dans l'ordre. Et il faut trouver une solution pour éviter cela.
La solution est de retarder les lectures dans le cache tant que tous les accès précédents ne sont pas terminés. Mais pour retarder les lectures en question, il faut d'abord savoir si la lecture atterrit dans le cache ou non, ce qui demande d'accéder au cache. On fait face à un dilemme : on veut retarder les accès au cache, mais les différencier des lectures déclenchant des accès mémoire demande d'accéder au cache en premier lieu. La solution est décrite dans l'article "Prefetching in a Texture Cache Architecture" par Igehy et ses collègues. Elle se base sur deux idées combinées ensemble.
La première idée est de séparer l'accès au cache en deux : une étape qui vérifie si les texels à lire sont dans le cache, et une étape qui accède aux données dans le cache lui-même. Un cache de texture est donc composé de deux circuits principaux. Le premier vérifie la présence des texels dans le cache. Il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Ensuite, en plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
La seconde idée est de retarder l'accès au cache entre les deux phases. La première étape d'un accès mémoire vérifie si la donnée est dans le cache ou non. Puis, on retarde la lecture des données, pour attendre que toutes les lectures précédentes soient terminées. Et enfin, troisième étape : la lecture des texels dans la mémoire cache proprement dite. Les accès mémoire passant par la mémoire vidéo se font de la même manière, à une différence près : la lecture dans le cache est remplacée par la lecture en mémoire vidéo. Tout démarre avec une demande à l'unité de tags, qui vérifie si le texel est dans le cache ou non. Puis on retarde l'accès tant que la mémoire vidéo est occupée, puis on effectue la lecture en mémoire vidéo.
Si ce n'est pas le cas, l'accès mémoire est envoyé à la mémoire vidéo comme précédemment, à savoir qu'il est mis en attente dans une mémoire FIFO, puis envoyé à la mémoire vidéo dès que celle-ci est libre. Mais en sortie de la mémoire, la donnée lue est envoyée dans le cache de texture, par dans l'unité de filtrage. Pour savoir où placer la donnée lue, l'unité de tag a réservé une ligne de cache précise, une adresse bien précise. L'adresse en question est disponible en lisant une autre mémoire FIFO, qui a mis en attente l'adresse en question, en attendant que l'accès mémoire se termine. La donnée est alors écrite dans le cache, puis lue par l'unité de filtrage de textures.
Pour une lecture dans le cache, le déroulement est similaire, mais sans le passage par la mémoire. La lecture fait une demande à l'unité de tag, et celle-ci répond que la donnée est bien dans le cache. Elle place alors l'adresse à lire dans la file d'attente. Une fois que les accès mémoire précédents sont terminés, l'adresse sort de la file d'attente et est envoyée à la mémoire de données. La lecture s'effectue, les texels sont envoyés à l'unité de filtrage de textures. La seule différence avec un ''phased cache'' normal est l'insertion de l'adresse à lire dans une FIFO qui vise à mettre en attente
[[File:Unité de texture avec un cache de texture.png|centre|vignette|upright=2.0|Unité de texture avec un cache de texture]]
Pour résumer, l'implémentation précédente garantit une exécution des lectures dans leur ordre d'arrivée. Et pour cela, elle retarde les lectures dans le cache tant que les lectures en mémoire précédentes ne sont pas terminées. L'accès au cache est plus rapide que l'accès en mémoire vidéo, mais le retard ajouté pour garantir l'ordre des lectures fait que le temps d'accès est très long.
==Le filtrage de textures==
Plaquer des textures sans autre forme de procès ne suffit pas à garantir des graphismes d'une qualité époustouflante. La raison est que les sommets et les texels ne tombent pas tout pile sur un pixel de l'écran : le sommet associé au texel peut être un petit peu trop en haut, ou trop à gauche, etc. Une explication plus concrète fait intervenir les coordonnées de texture. Souvenez-vous que lorsque l'on traduit une coordonnée de texture u,v en coordonnées x,y, on obtient un résultat qui ne tombe pas forcément juste. Souvent, le résultat a une partie fractionnaire. Si celle-ci est non-nulle, cela signifie que le texel/sommet n'est pas situé exactement sur le pixel voulu et que celui-ci est situé à une certaine distance. Concrètement, le pixel tombe entre quatre texels, comme indiqué ci-dessous.
[[File:Filtrage texture.png|centre|vignette|upright=2.0|Position du pixel par rapport aux texels.]]
Pour résoudre ce problème, on doit utiliser différentes techniques d'interpolation, aussi appelées techniques de '''filtrage de texture''', qui visent à calculer la couleur du pixel final en fonction des texels qui l'entourent. Il existe de nombreux types de filtrage de textures, qu'il s'agisse du filtrage linéaire, bilinéaire, trilinéaire, anisotropique et bien d'autres.
Tous ont besoin d'avoir certaines informations qui sont généralement fournies par les circuits de calcul d'adresse. La première est clairement la partie fractionnaire des coordonnées x,y. La seconde est la dérivée de ces deux coordonnées dans le sens horizontal et vertical., ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Toujours est-il que le filtrage de texture est une opération assez lourde, qui demande beaucoup de calculs arithmétiques. On pourrait en théorie le faire dans les pixels shaders, mais le cout en performance serait absolument insoutenable. Aussi, les cartes graphiques intègrent toutes un circuit dédié au filtrage de texture, le ''texture sampler''. Même les plus anciennes cartes graphiques incorporent une unité de filtrage de texture, ce qui nous montre à quel point cette opération est importante.
[[File:Texture unit.png|centre|vignette|upright=2.0|Unité de texture.]]
On peut configurer la carte graphique de manière à ce qu'elle fasse soit du filtrage bilinéaire, soit du filtrage trilinéaire, on peut configurer le niveau de filtrage anisotropique, etc. Cela peut se faire dans les options de la carte graphique, mais cela peut aussi être géré par l'application. La majorité des jeux vidéos permettent de régler cela dans les options. Ces réglages ne concernent pas la texture elle-même, mais plutôt la manière dont l'unité de texture doit fonctionner. Ces réglages sur l''''état de l'unité de texture''' sont mémorisés quelque part, soit dans l'unité de texture elle-même, soit fournies avec la ressource de texture elle-même, tout dépend de la carte graphique. Certaines cartes graphiques mémorisent ces réglages dans les unités de texture ou dans le processeur de commande, et tout changement demande alors de réinitialiser l'état des unités de texture, ce qui prend un peu de temps. D'autres placent ces réglages dans les ressources de texture elles-mêmes, ce qui rend les modifications de configuration plus rapides, mais demande plus de circuits. D'autres cartes graphiques mélangent les deux options, certains réglages étant globaux, d'autres transmis avec la texture. Bref, difficile de faire des généralités, tout dépend du matériel et le pilote de la carte graphique cache tout cela sous le tapis.
Maintenant que cela est dit, voyons quelles sont les différentes méthodes de filtrage de texture et comment la carte graphique fait pour les calculer.
===Le filtrage au plus proche===
La méthode de filtrage la plus simple consiste à colorier avec le texel le plus proche. Cela revient tout simplement à ne pas tenir compte de la partie fractionnaire des coordonnées x,y, ce qui est très simple à implémenter en matériel. C'est ce que l'on appelle le '''filtrage au plus proche''', aussi appelé ''nearest filtering''.
Autant être franc, le résultat est assez pixelisé et peu agréable à l’œil. Par contre, le résultat est très rapide à calculer, vu qu'il ne demande aucun calcul à proprement parler. Elle ne fait pas appel à la parti fractionnaire des coordonnées entières de texture, ni aux dérivées de ces coordonnées. On peut combiner cette technique avec le mip-mapping, ce qui donne un résultat bien meilleur, bien que loin d'être satisfaisant. Au passage, toutes les techniques de filtrage de texture peuvent se combiner avec du mip-mapping, certaines ne pouvant pas faire sans.
[[File:Interpolation-nearest.svg|centre|vignette|Filtrage de texture au plus proche.]]
===Le filtrage linéaire===
Le filtrage le plus simple est le '''filtrage linéaire'''. Il effectue une interpolation linéaire entre deux mip-maps, deux niveaux de détails. Pour comprendre l'idée, nous allons prendre une situation très simple, avec une texture carrée de 512 texels de côté. Le mip-mapping crée plusieurs textures : une de 256 texels de côté, une de 128 texels, une de 64, etc. Maintenant, la texture est sur un objet à une certaine distance de l'écran, vu de face. Le résultat est qu'elle correspond à l'écran à un carré de 300 pixels de côté (pas d'erreur : pixels, pas texels). Dans ce cas, la texture se trouve entre deux mip-maps : celle de 512 pixels de côté, celle de 256. Laquelle choisir ? Le filtrage au plus proche prend la texture de 512 pixels de côté. Le filtrage linéaire lui, fait autrement.
Vu que la texture est entre deux mip-maps, l'idée est de prendre le texel au plus proche dans chaque texture et de faire une sorte de moyenne appelée l'interpolation linéaire. L'interpolation par du principe que la couleur varie entre les deux texels en suivant une fonction affine, illustrée ci-dessous. Ce ne serait évidemment pas le cas dans le monde réel, mais on supposer cela donne une bonne approximation de ce à quoi ressemblerait une texture à plus haute résolution. On peut alors calculer la couleur du pixel par une simple moyenne pondérée par la distance. Le résultat est que les transitions entre deux niveaux de détails sont plus lisses, moins abruptes.
[[File:Lin interp -é.png|centre|vignette|upright=2.0|Interpolation linéaire.]]
===Le filtrage bilinéaire===
Le filtrage bilinéaire effectue une sorte de moyenne pondérée des quatre texels les plus proches du pixel à afficher. Pour cela, rappelez-vous ce qui a été dit plus haut : les coordonnées x,y d'un pixel ont une partie entière et une partie fractionnaire. Le filtrage au plus proche élimine les parties fractionnaires, ce qui donne une coordonnée x,y. Avec le filtrage bilinéaire, on prend les texels de coordonnées (x,y) ; (x+1,y) ; (x,y+1) ; (x+1,y+1), le pixel étant entre ces 4 texels.
Mais le filtrage ne fait pas qu'une simple moyenne, il prend en compte les parties fractionnaires pour faire la moyenne. En effet, le pixel n'est pas au milieu du carré de texel, il est quelque part mais est souvent plus proche d'un texel que des autres. Et il faut donc pondérer la moyenne par les distances aux 4 texels. Pour cela, la moyenne est calculée à partir d'interpolations linéaires. Avec 4 pixels, nous allons devoir calculer la couleur de deux points intermédiaires. La couleur de ces deux points se calcule par interpolation linéaire, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.
[[File:Bilin3.png|centre|vignette|upright=2|Filtrage bilinéaire de texture.]]
Le circuit qui permet de faire l'interpolation bilinéaire est particulièrement simple. On trouve un circuit de chaque pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu, et un pour la transparence. Chacun de ces circuit est composé de sous-circuits chargés d'effectuer une interpolation linéaire, reliés comme suit.
[[File:Texture sampler unit.png|centre|vignette|Unité de filtrage bilinéaire.]]
Vous noterez que le filtrage bilinéaire accède à 4 pixels en même temps. Fort heureusement, les textures sont stockées de manière à ce qu'on puisse charger les 4 pixels en une fois, comme on l'a vu plus haut. Le filtrage bilinéaire a de fortes chances que les 4 pixels filtrés soient dans la même ''tile'', la seule exception étant quand ils sont tout juste sur le bord d'une ''tile''.
: La console de jeu Nintendo 64 n'utilise que trois pixels au lieu de quatre dans son interpolation bilinéaire, qui en devient une interpolation quasi-bilinéaire. La raison derrière ce choix est une question de performances, comme beaucoup de décisions de ce genre. Le résultat est un rendu imparfait de certaines textures.
===Le filtrage trilinéaire===
Avec le filtrage bilinéaire, des discontinuités apparaissent sur certaines surfaces. Par exemple, pensez à une texture de sol : elle est appliquée plusieurs fois sur toute la surface du sol. A une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512*512 à 256*256, ce qui est visible pour un joueur attentif. De telles transitions sont lissées grâce au filtrage linéaire, il n'y a plus qu'à le combiner avec le filtrage bilinéaire. Rien d’incompatible : le premier filtre l'intérieur d'une mip-map, le second combine deux mip-maps.
Le filtrage trilinéaire prend les deux mip-maps les plus proches, fait un filtrage bilinéaire avec chacune, puis fait une « une moyenne » pondérée entre les deux résultats. Le circuit de filtrage trilinéaire existe en plusieurs versions. La plus simple, illustrée ci-dessous, effectue deux filtrages bilinéaires en parallèle, dans deux circuits séparés, puis combine leurs résultats avec un circuit d'interpolation linéaire. Mais ce circuit nécessite de charger 8 texels simultanément. Qui plus est, ces 8 texels ne sont pas consécutifs en mémoire, car ils sont dans deux niveaux de détails/mip-maps différents.
[[File:Parallel trilinear filtering.png|centre|vignette|upright=2.0|Unité de filtrage trilinéaire parallèle.]]
Vu qu'on lit des texels dans deux mip-maps, les texels sont lus en deux fois : 4 texels provenant de la première mip-map, suivis par les 4 texels de l'autre mip-map. Les 4 premiers texels doivent donc être mis en attente dans des registres, en attendant que les 4 autres arrivent. Une amélioration du circuit précédent gère cela en ajoutant des registres. Il lit les 4 premiers texels, les filtre avec une interpolation bilinéaire, et mémorise le résultat dans un registre. Puis, il lit les 4 autres texels, les filtre, et met le résultat dans un second registre. A ce moment là, un circuit d'interpolation linéaire finit le travail. On économise donc un circuit d'interpolation bilinéaire, sans que les performances soient trop impactées.
[[File:Filtrage trilineaire.png|centre|vignette|upright=1.0|Unité de filtrage trilineaire série.]]
Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le temps où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. À chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients, et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.
===Le filtrage anisotrope===
D'autres artefacts peuvent survenir lors de l'application d'une texture, la perspective pouvant déformer les textures et entraîner l'apparition de flou. La raison à cela est que les techniques de filtrage de texture précédentes partent du principe que la texture est vue de face. Prenez une texture carrée, par exemple. Vue de face, elle ressemble à un carré sur l'écran. Mais tournez la caméra, de manière à voir la texture de biais, avec un angle, et vous verrez que la forme de la texture sur l'écran est un trapèze, pas un carré. Cette déformation liée à la perspective n'est pas prise en compte par les méthodes de filtrage de texture précédentes. Pour le dire autrement, les techniques de filtrage précédentes partent du principe que les 4 texels qui entourent un pixel forment un carré, ce qui est vrai si la texture est vue de face, sans angle, mais ne l'est pas si la texture n'est pas perpendiculaire à l'axe de la caméra. Du point de vue de la caméra, les 4 texels forment un trapèze d'autant moins proche d'un carré que l'angle est grand.
Pour corriger cela, les chercheurs ont inventé le '''filtrage anisotrope'''. En fait, je devrais plutôt dire : LES filtrages anisotropes. Il en existe un grand nombre, dont certains ne sont pas utilisés dans les cartes graphiques actuelles, soit car ils trop gourmand en accès mémoires et en calculs pour être efficaces, soit car ils ne sont pas pratiques à mettre en œuvre. Il est très difficile de savoir quelles sont les techniques de filtrage de texture utilisées par les cartes graphiques, qu'elles soient récentes ou anciennes. Beaucoup de ces technologies sont brevetées ou gardées secrètes, et il faudrait vraiment creuser les brevets déposés par les fabricants de GPU pour en savoir plus. Les algorithmes en question seraient de plus difficiles à comprendre, les méthodes mathématiques cachées derrière ces méthodes de filtrage n'étant pas des plus simple.
[[File:Anisotropic filtering en.png|centre|vignette|upright=2|Exemple de filtrage anisotrope.]]
==La compression de textures==
Les textures les plus grosses peuvent aller jusqu'au mébioctet, ce qui est beaucoup. Pour limiter la casse, les textures sont compressées. La '''compression de texture''' réduit la taille des textures, ce qui peut se faire avec ou sans perte de qualité. Elle entraîne souvent une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande. Mais il s'agit là d'une technique très simple, beaucoup plus simple que les techniques que nous allons voir dans cette section. Nous allons voir quelque algorithmes de compression de textures de complexité intermédiaire, mais n'allons pas voir l'état de l'art. Il existe des formats de texture plus récents que ceux qui nous allons aborder, comme l{{'}}''Ericsson Texture Compression'' ou l{{'}}''Adaptive Scalable Texture Compression'', plus complexes et plus efficaces.
Notons que les textures sont compressées dans les fichiers du jeu, mais aussi en mémoire vidéo. Les textures sont décompressées lors de la lecture. Pour cela, la carte graphique contient alors un circuit, capable de décompresser les textures lorsqu'on les lit en mémoire vidéo. Les cartes graphiques supportent un grand nombre de formats de textures, au niveau du circuit de décompression. Du fait que les textures sont décompressées à la volée, les techniques de compression utilisées sont assez particulières. La carte graphique ne peut pas décompresser une texture entière avant de pouvoir l'utiliser dans un ''pixel shader''. A la place, on doit pouvoir lire un morceau de texture, et le décompresser à la volée. On ne peut utiliser les méthodes de compression du JPEG, ou d'autres formats de compression d'image. Ces dernières ne permettent pas de décompresser une image morceau par morceau.
Pour permettre une décompression/compression à la volée, les textures sont des textures tilées, généralement découpées en tiles de 4 * 4 texels. Les ''tiles'' sont compressées indépendamment les unes des autres. Et surtout, avec ou sans compression, la position des tiles en mémoire ne change pas. On trouve toujours une tile tous les T octets, peu importe que la tile soit compressée ou non. Par contre, une tile compressée n'occupera pas T octets, mais moins, là où une tile compressée occupera la totalité des T octets. En clair, compresser une tile fait qu'il y a des vides entre deux tiles dans al mémoire vidéo, mais ne change rien à leur place en mémoire vidéo qui est prédéterminée, peu importe que la texture soit compressée ou non. L'intérêt de la compression de textures n'est pas de réduire la taille de la texture en mémoire vidéo, mais de réduire la quantité de données à lire/écrire en mémoire vidéo. Au lieu de lire T octets pour une tile non-compressée, on pourra en lire moins.
===La palette indicée et la technique de ''Vector quantization''===
La technique de compression des textures la plus simple est celle de la '''palette indicée''', que l'on a entraperçue dans le chapitre sur les cartes d'affichage. La technique de '''''vector quantization''''' peut être vue comme une amélioration de la palette, qui travaille non pas sur des texels, mais sur des ''tiles''. À l'intérieur de la carte graphique, on trouve une table qui stocke toutes les ''tiles'' possibles. Chaque ''tile'' se voit attribuer un numéro, et la texture sera composé d'une suite de ces numéros. Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l’embarqué utilisent ce genre de compression.
===Les algorithmes de ''Block Truncation coding''===
La première technique de compression élaborée est celle du '''''Block Truncation Coding''''', qui ne marche que pour les images en niveaux de gris. Le BTC ne mémorise que deux niveaux de gris par ''tile'', que nous appellerons couleur 1 et couleur 2, les deux niveaux de gris n'étant pas le même d'une ''tile'' à l'autre. Chaque pixel d'une ''tile'' est obligatoirement colorié avec un de ces niveaux de gris. Pour chaque pixel d'une ''tile'', on mémorise sa couleur avec un bit : 0 pour couleur 1, et 1 pour couleur 2. Chaque ''tile'' est donc codée par deux entiers, qui codent chacun un niveau de gris, et une suite de bits pour les pixels proprement dit. Le circuit de décompression est alors vraiment très simple, comme illustré ci-dessous.
[[File:Block Truncation coding.jpg|centre|vignette|upright=2.0|Block Truncation coding.]]
La technique du BTC peut être appliquée non pas du des niveaux de gris, mais pour chaque composante Rouge, Vert et Bleu. Dans ces conditions, chaque ''tile'' est séparée en trois sous-''tiles'' : un sous-bloc pour la composante verte, un autre pour le rouge, et un dernier pour le bleu. Cela prend donc trois fois plus de place en mémoire que le BTC pur, mais cela permet de gérer les images couleur.
===Le format de compression S3TC / DXTC===
L'algorithme de '''Color Cell Compression''', ou CCC, améliore le BTC pour qu'il gère des couleurs autre que des niveaux de gris. Ce CCC remplace les deux niveaux de gris par deux couleurs. Une ''tile'' est donc codée avec un entier 32 bits par couleur, et une suite de bits pour les pixels. Le circuit de décompression est identique à celui utilisé pour le BTC.
[[File:Color Cell Compression.jpg|centre|vignette|Color Cell Compression.]]
[[File:Dxt1-memory-layout.png|vignette|Dxt1 et ''color cell compression''.]]
Le format de compression de texture utilisé de base par Direct X, le DXTC, est une version amliorée de l'algorithme précédent. Il est décliné en plusieurs versions : DXTC1, DXTC2, etc. La première version du DXTC est une sorte d'amélioration du CCC : il ajoute une gestion minimale de transparence, et découpe la texture à compresser en ''tiles'' de 4 pixels de côté. La différence, c'est que la couleur finale d'un texel est un mélange des deux couleurs attribuée au bloc. Pour indiquer comment faire ce mélange, on trouve deux bits de contrôle par texel.
Si jamais la couleur 1 < couleur2, ces deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (2 * Couleur1 + Couleur2) / 3
* 11 = (Couleur1 + 2 * Couleur2) / 3
Sinon, les deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (Couleur1 + Couleur2) / 2
* 11 = Transparent
[[File:DXTC.jpg|centre|vignette|DXTC.]]
Le circuit de décompression du DXTC ressemble alors à ceci :
[[File:Circuit de décompression du DXTC.jpg|centre|vignette|upright=2.0|Circuit de décompression du DXTC.]]
===Les format DXTC 2, 3, 4 et 5 : l'ajout de la transparence===
Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3, lui-même replacé par le DXT4 et par le DXT5. Dans le DXT3, la transparence fait son apparition. Pour cela, on ajoute 64 bits par ''tile'' pour stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1.
[[File:Dxt23-memory-layout.png|centre|vignette|Dxt 2 et 3.]]
Dans le DXT4 et le DXT5, la méthode utilisée pour compresser les couleurs l'est aussi pour les valeurs de transparence. L'information de transparence est stockée par un en-tête contenant deux valeurs de transparence, le tout suivi d'une matrice qui attribue trois bits à chaque texel. En fonction de la valeur des trois bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1.
[[File:Dxt45-memory-layout.png|centre|vignette|Dxt 4 et 5.]]
===Le format de compression PVRTC===
Passons maintenant à un format de compression de texture un peu moins connu, mais pourtant omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle travaille surtout dans les cartes graphiques embarquées. Ses cartes se trouvent notamment dans l'ipad, l'iPhone, et bien d'autres smartphones actuels.
Avec le PVRTC, les textures sont encore une fois découpées en ''tiles'' de 4 texels par 4, mais la ressemblance avec le DXTC s’arrête là. Chacque ''tile'' est codée avec :
* une couleur codée sur 16 bits ;
* une couleur codée sur 15 bits ;
* 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
* et un bit de modulation, qui permet de configurer l’interprétation des bits de mélange.
Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4*4.
==Annexe : le ''normal-mapping'' hardware==
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, parlons un peu du ''normal mapping'' et de son implémentation dans les unités de texture. Pour rappel, 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. Et pour comprendre quel est le rapport avec les textures, nous allons devoir faire quelques rappels sur l'éclairage par pixel.
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
L'éclairage se fait en utilisant de nombreux calculs, qu'on a détaillé dans le chapitre sur les bases du rendu 3D. Ceux-ci utilisent la normale d'un sommet, à savoir un vecteur orienté à la verticale de la surface. En théorie, il y a une normale par sommet, ce qui fait que les calculs d'éclairage doivent se faire au niveau géométrique, avant la rastérisation. Mais pour obtenir un éclairage de meilleure qualité, il y a des techniques qui calcule l'éclairage d'une scène 3D pixel par pixel.
Une première technique d'éclairage par pixel interpole les normales lors de l'étape de rastérisation. On obtient alors un éclairage de Phong. Une autre solution est celle du ''normal mapping''. Le '''''normal mapping''''' précalcule les normales d'une surface dans une texture, appelée la ''normal map''. L'éclairage est alors réalisé par un pixel shader, qui lit les normales depuis cette texture et fait les calculs d'éclairage avec.
===Les ''normal maps'' avec des textures usuelles===
La ''normal map'' est une texture, donc. Mais ce n'est pas une texture comme une autre. Elle mémorise un vecteur pour chaque texel, pas une couleur. Il est possible d'utiliser les formats de textures vus plus haut, même si cela ne donne pas de bons résultats. Après tout, un vecteur est codé par trois coordonnées x,y,z, il est possible de coder chacune avec un octet et de packager cela dans une texture RGB classique. La coordonnée x va dans la composante Rouge, la coordonnée Y dans la composante Bleu, et la coordonnée z dans la composante z.
Une première optimisation est de ne pas mémoriser les trois coordonnées, mais seulement deux d'entre elles et de calculer la troisième. En effet, les normales sont des vecteurs ''normalisés'', c'est à dire que leur longueur vaut 1, par construction. Vu que la taille est connue à l'avance, on peut en déduire la coordonnée z à partir des coordonnées x et y, avec l'usage du théorème de Pythagore. Par définition, <math>z^2 = 1 - x^2 - y^2</math>. Le calcul peut être fait dans le ''pixel shader'' sans problème.
: Pour être précis, il faut que les normales soient définies d'une certaine manière pour que ça marche. Les normales ne sont pas définies dans le même système de coordonnées que le modèle 3D, mais dans un autre système appelé l'''espace tangent''.
Notons que l'accès à la ''normal map'' se fait comme pour n'importe quelle texture : les texels sont lus en mémoire vidéo, puis le filtrage de texture est appliqué, et enfin le tout est envoyé au ''pixel shader''. En théorie, le filtrage effectue une sorte d'interpolation des normales automatique, ce qui permet de trouver la normale à un pixel précis, même s'il n'est pas sur un texel.
===Le format de texture 3Dc===
Le problème est que le rendu final n'est pas très beau. Et les opérations de filtrage ne donnent pas de très bons résultats. Pour remédier à ces problèmes, des formats de texture spécialisés pour les ''normal map'' ont été inventés. Le premier d'entre eux est le '''3Dc''', aussi appelé BC5 pour ''Block Compression 5''. Il utilise un octet par texel, au lieu de trois avec une texture RGB normale non-compressée.
Avec le 3Dc, seules coordonnées sont mémorisées, la troisième est calculée, comme dit plus haut. Pour le reste, la compression de la ''normal map'' ressemble un peu à celle des textures. La ''normal map'' est découpée en blocs de 4 par 4 texels de côté, chacune étant encodée en tenant compte de certaines redondances. Dans un bloc, les coordonnées x sont dans un certain intervalle, allant de <math>x_{min}</math> à <math>x_{max}</math>, idem pour la coordonnée y qui est dans l'intervalle <math>y_{min}</math>, <math>y_{max}</math>. L'intervalle <math>x_{min}</math> à <math>x_{max}</math> est coupé en 8 parts égales, ce qui fait 8 valeurs possibles dans cet intervalle, chacune correspondant à une valeur x possible pour une normale du bloc. La normale est donc encodée en précisant quelle valeur est la bonne, en utilisant un indice de 3 bits pour cela. La valeur exacte de la coordonnée x se calcule à partir de l'indice comme suit :
: <math>x = x_{min} + \left( \frac{x_{min} - x_{min}}{8} \right) \times \text{indice} </math>
Pour encoder un bloc de 4 par 4 normales, il faut donc :
* Un octet pour chaque coordonnée <math>x_{min}</math>, <math>x_{max}</math>, <math>y_{min}</math>, <math>y_{max}</math>.
* 6 bits par normale : 3 pour encoder le décalage de la coordonnée x, 3 pour la coordonnée y.
Le tout permet d'encoder un bloc de 16 normales en seulement 128 bits, soit un octet par normale. Le ''pixel shader'' se débrouille pour décompresser un bloc. Il effectue notamment le calcul du dessus, pour retrouver la coordonnée x. Les calculs étant particulièrement simples, le cout en performance est très faible. Et ce d'autant plus qu'ils demandent juste de faire des additions et des multiplications entières, que l'unité scalaire peut faire en parallèle d'autres opérations plus gourmandes. Et le cout en calculs est de toute façon compensé par l'économie de mémoire et de bande passante lié à la compression : diviser la taille des données par trois, ca a un sacré impact !
==Annexe : les ''shadowmap'' hardware==
Les anciens GPU, notamment la Geforce FX, avaient des fonctionnalités spécifiques pour le calcul des ombres. Dans la plupart des jeux vidéos de l'époque, et même de maintenant, les ombres sont calculées avec la technique des ''shadowmap''. L'idée est assez simple sur le principe : un pixel est dans l'ombre quand il est invisible depuis une source de lumière.
L'idée est que le rendu est réalisé en plusieurs passes, avec une passe par source de lumière et une passe finale pour calculer l'image finale. Nous allons expliquer la technique avec une seule source de lumière, et allons utiliser l'exemple de la scène ci-dessous.
[[File:7fin.png|centre|vignette|Scène 3D d'exemple.]]
===La technique du ''shadowmapping''===
[[File:2shadowmap.png|vignette|Résultat de la première passe : ''shadowmap''..]]
La première passe rend l'image depuis le point de vue de la source de lumière. Cette première passe ne rend pas les couleurs de la scène, elle ne s'intéresse qu'à la profondeur des pixels. Le résultat est que l'image ne rend que le tampon de profondeur. Celui-ci est ensuite réutilisé comme texture pour la passe suivante. La texture en question est appelée la '''''shadownmap'''''.
La perspective utilisée, ainsi que le ''view frustrum'', dépend de la source de lumière. Pour une source de lumière qui émet un cône de lumière, le ''view frustrum'' de l'image rendue doit contenir tout le cone de lumière, et doit coller le plus possible à celui-ci. Pour une source directionnelle, comme le soleil, une perspective orthographique est utilisée.
La seconde passe rend l'image du point de vue de la caméra, pour rendre l'image finale. Elle rend l'image finale, qui est composée de pixels, chacun ayant une position à l'écran x,y, et une profondeur z. Les coordonnées sont transformées pour obtenir la position de ce pixel depuis le point de vue de la caméra. Une simple multiplication de matrice suffit, rien de bien compliqué, un shader peut le faire.
[[File:5failed.png|vignette|Résultat du test des comparaisons.]]
Après cette étape, on a alors les coordonnées x,y,z de ce pixel du point de vue de la caméra, et la ''shadowmap''. Il est alors possible d'accéder à la ''shadowmap'' au même endroit, à la même place que le pixel testé, aux mêmes coordonnées x,y. Si la profondeur du pixel est supérieure à celle de la shadowmap au même endroit, alors le pixel est situé derrière la surface visible, donc est dans l'ombre. Sinon, il n'est pas dans l'ombre. Le même procédé est répété sur chaque pixel de l'écran.
===Les optimisations hardware du ''shadowmapping''===
La technique des ''shadowmap'' demande donc de calculer une texture ''shadowmap'', puis de lire celle-ci et de faire des comparaisons de profondeur. Les GPU comme la Geforce FX intégraient du matériel dans les unités de texture pour faciliter ce travail. Les unités de texture pouvaient lire les ''shadowmap'', et faire la comparaison de profondeur toutes seules, elles avaient des circuits pour. Il suffisait de leur fournir le pixel à tester, ses coordonnées x,y,z, et l'adresse de la ''shadowmap''. Les unités de texture renvoyaient alors un résultat valant 0 ou 1 : 1 si le pixel est dans l'ombre, 0 sinon.
Elles pouvaient même effectuer du filtrage de texture sur les ''shadowmap''. Mais le filtrage était différent de celui utilisé sur les autres textures : moyenner des valeurs de profondeur ne marche pas bien. Elles utilisaient des techniques de filtrage différentes : elles faisaient les tests de comparaison, puis faisaient la moyenne des résultats. Ainsi, pour du filtrage bilinéaire, elles lisaient 4 texels dans la ''shadowmap'', puis faisaient 4 tests de comparaison, et moyennaient les 4 résultats.
==Annexe : les textures virtuelles==
Les '''textures virtuelles''' sont une optimisation des textures normales, qui visent à accélérer le rendu de terrains de grande taille. Imaginez par exemple un monde assez ouvert, comme un environnement en forêt ou en montagne, avec une grande distance de visibilité. Avec de tels terrains, le "sol" est recouvert par une texture de sol unique qui recouvre tout le terrain. Elle ne se répète pas, est de très grande taille, et peut parfois recouvrir toute la map ! Mais il n'y a pas assez de mémoire vidéo pour mémoriser la texture toute entière. La seule solution est la suivante : une partie de la texture est placée en mémoire vidéo, le reste est soit placé en mémoire RAM ou sur le disque dur.
Pour cela, le moteur de jeu utilise une optimisation ingénieuse, basée sur une observation assez basique : une bonne partie de la texture est visible, mais le reste est caché par des arbres, des habitations ou d'autres obstacles. Une optimisation possible de ne garder en mémoire vidéo que les portions visibles de la texture, pas les portions cachées. Une autre optimisation mélange textures virtuelles et ''mip-mapping''. L'idée est que pour les portions lointaines d'une texture, la texture utilisée est une ''mip-map'' de basse résolution. L'idée est alors de ne charger que la ''mip-map'' adéquate, pas les autres niveaux de détail. En clair, la texture de base n'est pas chargée en mémoire vidéo, mais la ''mip-map'' basse résolution l'est.
===Une texture à deux niveaux===
L'implémentation des textures virtuelles découpe les méga-textures en ''tiles'', en morceaux rectangulaires de taille modeste. En clair, le terrain est découpé en morceau rectangulaires/carrés. Seules les tiles nécessaires sont chargées en mémoire vidéo, pas les autres. Par exemple, les ''tiles'' non-visibles ne sont pas placées en mémoire vidéo, seules les ''tiles'' visibles le sont. De même, il y a une ''tile'' par niveau de mip-map : seul la tile correspondant le niveau adéquat est en mémoire vidéo, les autres niveaux de détail ne sont pas chargés. On peut faire une analogie avec la mémoire virtuelle, où les données sont découpées en pages, qui sont chargées en mémoire RAM à la demande, suivant les besoins, les données pouvant être swappées sur le disque dur si elles sont peu utilisées. Sauf qu'ici, il s'agit de textures qui sont découpées en pages chargées à la demande en mémoire vidéo, depuis la RAM système.
Une texture virtuelle est en réalité un système à deux niveaux : une liste de ''tiles'' et les ''tiles'' elles-mêmes. La liste de ''tiles'' est appelée un '''atlas de texture''', c'est un peu l'équivalent de la ''tilemap'' pour le rendu 2D. Rendre une texture demande de calculer quelle ''tile'' contient le texel à afficher, consulter la ''tile'' en question, puis récupérer le texel adéquat dans cette ''tile''. La ''tile'' est donc une texture, mais la texture à charger est choisie parmi un ensemble, qui est ici l'atlas de texture.
===L'implémentation : logicielle versus matérielle===
Les textures virtuelles ont été utilisées pour la première fois par les jeux Rage 1 et 2 d'IdSoftware, et quelques jeux ultérieurs comme DOOM 2016. IdSoftware les appelait des '''''mega-textures'''''. L'optimisation permettait des gains en performance assez impressionnants. Le jeu Rage 1 utilisait une texture carrée unique de 128k pixels de côté pour rendre le terrain. En théorie, une telle texture devrait prendre 64 giga-octets, mais le jeu tournait correctement avec 512 méga-octets de RAM, poussivement avec seulement 256 méga-octets de RAM.
De nos jours, les textures virtuelles sont supportées par beaucoup de jeux vidéos, les moteurs les plus courants gèrent de telles textures de manière logicielles. Mais quelques GPU récents supportent les textures virtuelles. Sur les GPU récents, l'atlas de texture est géré nativement par le matériel. Le GPU choisit quelle ''tile'', quelle texture choisir pour rendre le texel adéquat. Pour cela, le GPU calcule quelle ''tile'' charger, consulte l'atlas de texture, et lit la texture de ''tile'' adéquate.
Mais l'implémentation sur les GPU récents a de nombreuses limitations. La limitation la plus importante est que la taille des textures virtuelles ne peut pas dépasser la taille d'une texture normale, soit 32768 pixels de côté pour une texture carrée environ sur les GPU de 2020. De plus, le chargement d'une ''tile'' est très lent. En clair, dès qu'on veut changer de niveau de mip-map pour une tile, ou dès qu'une tile devient visible, le chargement de la tile peut facilement prendre plusieurs centaines de millisecondes. Le filtrage de texture est très complexe avec des textures virtuelles, ce qui fait que le filtrage de texture virtuelle est souvent soumis à des limitations que les textures normales n'ont pas, notamment pour le filtrage anisotropique.
{{NavChapitre | book=Les cartes graphiques
| prev=Le rasterizeur
| prevText=Le rasterizeur
| next=Les Render Output Target
| nextText=Les Render Output Target
}}{{autocat}}
fmoo3xgebwhkz2kct6x1uou0wg8cx19
763416
763415
2026-04-10T18:29:09Z
Mewtow
31375
/* Les normal maps avec des textures usuelles */
763416
wikitext
text/x-wiki
[[File:Texture mapping.png|vignette|''Texture mapping'']]
Les '''textures''' sont des images que l'on va plaquer sur la surface d'un objet, du papier peint en quelque sorte. Les cartes graphiques supportent divers formats de textures, qui indiquent comment les pixels de l'image sont stockés en mémoire : RGB, RGBA, niveaux de gris, etc. Une texture est donc composée de "pixels", comme toute image numérique. Pour bien faire la différence entre les pixels d'une texture, et les pixels de l'écran, les pixels d'une texture sont couramment appelés des ''texels''.
==Le placage de textures inverse==
Pour rappel, plaquer une texture sur un objet consiste à attribuer un texel à chaque sommet, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture.
Dans les faits, on n'utilise pas de coordonnées entières de ce type. Les coordonnées de texture sont deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. Le nom donnée à cette technique de description des coordonnées de texture s'appelle l''''''UV Mapping'''''.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Les API 3D modernes gèrent des textures en trois dimensions, ce qui ajoute une troisième coordonnée de texture notée w. Dans ce qui va suivre, nous allons passer les textures en trois dimensions sous silence. Elles ne sont pas très utilisées, la quasi-totalité des jeux vidéo et applications 3D utilisant des textures en deux dimensions. Par contre, le matériel doit gérer les textures 3D, ce qui le rend plus complexe que prévu. Il faut ajouter quelques circuits pour, de quoi gérer la troisième coordonnée de texture, etc.
Lors de la rastérisation, chaque fragment se voit attribuer un sommet, et donc la coordonnée de texture qui va avec. Si un pixel est situé pile sur un sommet, la coordonnée de texture de ce sommet est attribuée au pixel. Si ce n'est pas le cas, la coordonnée de texture finale est interpolée à partir des coordonnées des trois sommets du triangle rastérisé. L'interpolation en question a lieu dans l'étape de rastérisation, comme nous l'avons vu dans le chapitre précédent. Le fait qu'il y ait une interpolation fait que les coordonnées du pixel gagent à être des nombres flottants. On pourrait faire une interpolation avec des coordonnées de texture entières, mais les arrondis et autres imprécisions de calcul donneraient un résultat graphiquement pas terrible, et empêcheraient d'utiliser les techniques de filtrage de texture que nous verrons dans ce chapitre.
À partir de ces coordonnées de texture, la carte graphique calcule l'adresse du texel qui correspond, et se charge de le lire. Et toute la magie a lieu dans ce calcul d'adresse, qui part de coordonnées de texture flottante, pour arriver à une adresse mémoire. Le calcul de l'adresse du texel se fait en plusieurs étapes, que nous allons voir ci-dessous. La première étape convertit les coordonnées flottantes en coordonnées entières, qui disent à quel ligne et colonne se trouve le texel voulu dans la texture. L'étape suivante transforme ces coordonnées x,y entières en adresse mémoire.
===La normalisation des coordonnées===
J'ai dit plus haut que les coordonnées de texture sont des coordonnées flottantes, comprises entre 0 et 1. Mais il faut savoir que les pixels shaders peuvent modifier celles-ci pour mettre en œuvre certains effets graphiques. Et le résultat peut alors se retrouver en-dehors de l'intervalle 0,1. C'est quelque chose de voulu et qui est traité par la carte graphique automatiquement, sans que ce soit une erreur. Au contraire, la manière dont la carte graphique traite cette situation permet d'implémenter des effets graphiques comme des textures en damier ou en miroir.
[[File:Clamp tile.jpg|vignette|Clamp tile]]
Il existe globalement trois méthodes très simples pour gérer cette situation, qui sont appelés des '''modes d'adressage de texture'''.
* La première méthode est de faire en sorte que le résultat sature. Si une coordonnée est inférieur à 0, alors on la remplace par un zéro. Si elle est supérieure à 1, on la ramène à 1. Avec cette méthode, tout se passe comme si les bords de la texture étaient étendus et remplissaient tout l'espace autour de la texture. Le tout est illustré ci-dessous. Ce mode d'accès aux textures est appelé le '''''clamp'''''.
* Une autre solution retire la partie entière de la coordonnée, elle coupe tout ce qui dépasse 1. Pour le dire autrement, elle calcule le résultat modulo 1 de la coordonnée. Le résultat est que tout se passe comme si la texture était répétée à l'infini et qu'elle pavait le plan.
* Une autre méthode remplit les coordonnées qui sortent de l’intervalle 0,1 avec une couleur préétablie, configurée par le programmeur.
===La conversion des coordonnées de textures flottantes en adresse mémoire===
Une fois la normalisation effectuée, les coordonnées de texture sont utilisées pour lire le texel voulu. Pour cela, les coordonnées de texte sont transformées en adresse mémoire, adresse qui pointe sur le texel ayant ces cordonnées. Pour cela, la première étape est de transformer les coordonnées flottantes u,v en coordonnées entières x,y qui pointent sur un texel. Pour cela, il suffit de multiplier les coordonnées flottantes u,v par la résolution de la texture accédée. Pour un écran de résolution <math>\text{height,width}</math>, le calcul est le suivant :
: <math>x = u \times \text{width}</math>
: <math>y = v \times \text{height}</math>
Le résultat est un nombre avec une partie entière et une partie fractionnaire. La partie entière des deux coordonnées donne la position x,y voulue, et la partie fractionnaire est conservée pour le filtrage de textures, mais passons cela sous silence pour le moment.
La seconde étape prend les coordonnées entières x,y et calcule l'adresse mémoire du texel. L'adresse dépend de la position de la texture en mémoire, précisément de son début, son premier texel, mais aussi de la position du texel par rapport au début de la texture. Et calculer cette position intra-texture dépend de la manière dont les texels sont stockés en mémoire.
====Les textures naïves====
Les programmeurs qui lisent ce cours s'attendent certainement à ce que la texture soit stockée en mémoire ligne par ligne, ou colonne par colonne. Cela veut dire que le premier pixel en partant d'en haut à gauche est stocké en premier, puis celui immédiatement à sa droite, puis celui encore à droite, et ainsi de suite. Une fois qu'on arrive à la fin d'une ligne, on passe à la ligne suivante, en-dessous. Cette organisation ligne par ligne s'appele l'organisation '''''row major order'''''. On peut faire pareil, mais colonne par colonne, ce qui donne le '''''column major order'''''.
[[File:Speicheranordnung Feld.svg|centre|vignette|upright=2|Row et column major order.]]
Maintenant, supposons que la texture commence à l'adresse <math>A_\text{texture}</math>, qui est l'adresse du premier texel. La texture a une résolution de <math>\text{width}</math> texels de large et <math>\text{height}</math> texels de haut. Par définition, les coordonnées X et Y des texels commencent à 0, ce qui fait que le pixel en haut à gauche a les coordonnées 0,0.
L'adresse du pixel se calcule comme suit :
: <math>A_\text{pixel} = A_\text{texture} + (\text{taille d'une ligne en octets} \times Y) + (\text{taille d'un texel en octets} \times X)</math>
La taille d'un pixel en mémoire est notée T. La taille d'une ligne en mémoire est de <math>width \times T</math>, par définition, vu qu'elle fait <math>width</math> texels. On a donc :
: <math>A_\text{pixel} = A_\text{texture} + (width \times T \times Y) + (T \times X)</math>
La formule se réécrit comme suit :
: <math>A_\text{pixel} = A_\text{texture} + T \times (width \times Y + X)</math>
Le calcul d'adresse est donc assez simple. Malheureusement, les textures ne sont pas stockées de cette manière en mémoire vidéo. En effet, elle se marie mal avec les opérations de filtrage de texture que nous allons voir dans ce qui suit. Le filtrage d'un texel dépend de ses voisins du dessus et du dessous. Le fait que la texture n'est pas forcément parcourue ligne par ligne fait que stocker une texture ligne par ligne n'est pas l'idéal.
De même, les textures sont déformées par la perspective. L'affichage de la texture ne se fait alors pas ligne par ligne, mais en parcourant la texture en diagonale, l'angle de la diagonale correspondant approximativement à l'angle que fait la verticale de la texture avec le regard. Vu qu'on ne connait pas à l'avance l'angle que fera la diagonale de parcours, on doit ruser.
====Les textures tilées====
Une première solution à ce problème est celle des '''textures tilées'''. Avec ces textures, l'image de la texture est découpée en ''tiles'', des rectangles ou en carrés de taille fixe, généralement des carrés de 4 pixels de côté. Les tiles ont une largeur et une longueur égales, afin de simplifier les calculs : on divise X et Y par le même nombre. De plus, leur largeur et leur longueur sont une puissance de deux, afin de simplifier les calculs d'adresse. Les ''tiles'' sont alors mémorisée les unes après les autres dans le fichier de la texture.
[[File:Texture tilée.png|centre|vignette|upright=2|Texture tilée]]
La formule de calcul d'adresse vue plus haut doit être adaptée pour tenir compte des tiles. Pour cela, il faut remplacer la taille d'un texel par la taille d'une tile, et que la largeur de la texture soit exprimée en nombre de tiles. De plus, on doit adapter les coordonnées des texels pour donner des coordonnées de tile. Généralement, les tiles sont des carrés de N pixels de côté, ce qui fait qu'on peut regrouper les lignes et les colonnes par paquets de N. Il suffit donc de diviser Y et X pour obtenir les coordonnées de la tile, de même que la larguer. La formule pour calculer la position de la énième tile est alors la suivante :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + \text{Taille mémoire d'une tile} \times \left( {\text{Width} \over N} \times {Y \over N} + {X \over N} \right)</math>
On peut réécrire le tout comme suit :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + K \times \left( {Y \over N} + X \right)</math>, avec K une constante connue à la compilation des shaders.
Vu que les tiles sont carrées avec une largeur qui est une puissance de deux, la multiplication par la taille d'une tile en mémoire se simplifie : on passe d'une multiplication entière à des décalages de bits. Même chose pour le calcul de l'adresse de la tile à partir des coordonnées x,y : ils impliquent des divisions par une puissance de deux, qui deviennent de simples décalages.
La position d'un pixel dans une tile dépend du format de la texture, mais peut se calculer avec quelques calculs arithmétiques simples. Dans les cas les plus simples, les pixels sont mémorisés ligne par ligne, ou colonne par colonne. Mais ce n'est pas systématiquement le cas. Toujours est-il que les calculs pour déterminer l'adresse sont simples, et ne demandent que quelques additions ou multiplications. Mais avec les formats de texture utilisés actuellement, les tiles sont chargées en entier dans le cache de texture, sans compter que diverses techniques de compression viennent mettre le bazar, comme on le verra dans la suite de cours.
Un avantage de l'organisation en tiles est qu'elle se marie bien avec le parcours des textures. On peut parcourir une texture dans tous les sens, horizontal, vertical, ou diagonal, on sait que les prochains pixels ont de fortes chances d'être dans la même tile. Si on rentre dans une tile par la gauche en haut, on a encore quelques pixels à parcourir dans la tile, par exemple. De même, le filtrage de textures est facilité. On verra dans ce qui va suivre que le filtrage de texture a besoin de lire des blocs de 4 texels, des carrés de 2 pixels de côté. Avec l'organisation en tile, on est certain que les 4 texels seront dans la même tile, sauf s'ils ont le malheur d'être tout au bord d'une tile. Ce dernier cas est assez rare, et il l'est d'autant plus que les tiles sont grandes. Enfin, un dernier avantage est que les tiles sont généralement assez petites pour tenir tout entier dans une ligne de cache. Le cache de texture est donc utilisé à merveille, ce qui rend les accès aux textures plus rapides.
====Les textures basées sur des ''z-order curves''====
Les formats de textures théoriquement optimaux utilisent une '''''Z-order curve''''', illustrée ci-dessous. L'idée est de découper la texture en quatre rectangles identiques, et de stocker ceux-ci les uns à la suite des autres. L'intérieur de ces rectangles est lui aussi découpé en quatre rectangles, et ainsi de suite. Au final, l'ordre des pixels en mémoire est celui illustré ci-dessous.
[[File:Z-CURVE.svg|centre|vignette|upright=2|Construction d'une ''Z-order curve''.]]
Les texels sont stockés les uns à la suite des autres dans la mémoire, en suivant l'ordre donnée par la ''Z-order curve''. Le calcul d'adresse calcule la position du texel en mémoire, par rapport au début de la texture, et ajoute l'adresse du début de la texture. Mais tout le défi est de calculer la position d'un texel en mémoire, à partir des coordonnées x,y. Le calcul peut sembler très compliqué, mais il n'en est rien. Le calcul demande juste de regarder les bits des deux coordonnées et de les combiner d'une manière particulièrement simple. Il suffit de placer le bit de poids fort de la coordonnée x, suivi de celui de la coordonnée y, et de faire ainsi de suite en passant aux bits suivants.
[[File:Zcurve45bits.png|centre|vignette|upright=1.5|Calcul de la position d'un élément dans une ''Z-order curve'' à partir des coordonnées x et y.]]
L'avantage d'une telle organisation est que la textures est découpées en ''tiles'' rectangulaires d'une certaine taille, elles-mêmes découpées en ''tiles'' plus petites, etc. Et il se trouve que cette organisation est parfaite pour le cache de texture. L'idéal pour le cache de texture est de charger une ''tile'' complète dans le cache de textures. Quand on accède à un texel, on s'assure que la ''tile'' complète soit chargée. Mais cela demande de connaitre à l'avance la taille d'une ''tile''. Les formats de texture fournissent généralement une ''tile'' carré de 4 pixels de côté, mais cela donnerait un cache trop petit pour être vraiment utile. Avec cette méthode, on s'assure qu'il y ait une ''tile'' avec la taille optimale. Les ''tiles'' étant découpées en ''tiles'' plus petites, elles-mêmes découpées, et ainsi de suite, on s'assure que la texture est découpées en ''tiles'' de taille variées. Il y aura au moins une ''tile'' qui rentrera tout pile dans le cache.
==Les techniques de rendu à textures multiples==
Nous venons de voir comment une texture est plaquée sur un objet 3D, ou une surface comme un sol. Pour résumer, le calcul de l'adresse d'un texel prend la position du texel par rapport au début de la texture, et ajoute l'adresse du début de la texture. L'adresse mémoire de la texture est connue au moment où le pilote de la carte graphique place la texture dans la mémoire vidéo, et cette information est transmise au matériel par l'intermédiaire du processeur de commande, puis passée aux processeurs de shaders et à l'unité de texture. Le tout est couplé à d'autres informations, la plus importante étant la ''taille de la texture en octets'', pour éviter de déborder lors des accès à la texture.
Néanmoins, il s'agit là du cas le plus simple. Certaines techniques de rendu demandent de choisir la texture à plaquer parmi un ensemble de plusieurs textures. Les techniques en question sont assez variées et n'ont pas grand chose en commun. Les plus connues sont le ''mip-mapping'', le ''cube-mapping'' et les textures virtuelles. Le ''mip-mapping'' sert à filtrer les textures, chose qu'on expliquera plus tard, le ''cube-mapping'' sert à simuler des réflexions sur un objet en plaquant une texture de l'environnement dessus, les textures virtuelles sont une optimisation pour les textures des terrains de grande taille. Mais malgré leurs différences, elles demandent de choisir quelle texture plaquer entre plusieurs textures de base. En clair, l'adresse de base de la texture varie selon la situation. Voyons-les dans le détail.
===Le mip-mapping===
Le '''mip-mapping''' a pour but de légèrement améliorer les graphismes des objets lointains, tout en rendant les calculs de texture plus rapides. Formellement, le ''mip-mapping'' est une technique de filtrage de texture, mais nous l'abordons maintenant car elle est surtout liée au calcul d'adresse. Les unités de texture ont des circuits de filtrage de texture séparés des circuits de ''mip-mapping'' et de calcul d'adresse, d'où le fait que nous en parlons séparément.
Le problème résolu par le ''mip-mapping'' est le rendu des textures lointaines. Si une texture est plaquée sur un objet lointain, une bonne partie des détails est invisible pour l'utilisateur. Un pixel de l'écran est associé à plusieurs texels. Idéalement, la carte graphiques devrait lire tous ces texels et en faire une sorte de moyenne pondérée, pour calculer la couleur finale du pixel. Mais dans les faits, ce serait très gourmand et compliqué à implémenter en hardware. Une solution serait de ne garder que quelque texels, mais cela a tendance à créer des artefacts visuels (les textures affichées ont tendance à pixeliser). Le ''mip-mapping'' permet de réduire ces deux problèmes en même temps en précalculant cette moyenne pondérée pour des distances prédéfinies.
L'idée est d'utiliser plusieurs exemplaires d'une même texture à des résolutions différentes, chaque exemplaire étant adapté à une certaine distance. Par exemple, une texture sera stocké avec un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre de 128 * 128 et ainsi de suite jusqu’à un dernier exemplaire de 32 * 32 pixel. Chaque exemplaire correspond à un '''niveau de détail''', aussi appelé ''Level Of Detail'' (abrévié en LOD). La résolution utilisée diminue d'autant plus que l'objet est situé loin de la caméra. Les objets proches seront rendus avec la texture 512*512, ceux plus lointains seront rendus avec la texture de résolution 256*256, les textures 128*128 seront utilisées encore plus loin, et ainsi de suite jusqu'aux objets les plus lointains qui sont rendus avec la texture la plus petite de 32*32.
[[File:MipMap Example STS101.jpg|centre|vignette|upright=2|Exemples de mip-maps.]]
Le ''mip-mapping'' améliore grandement la qualité d'image. L'image d'exemple ci-dessous le montre assez bien.
[[File:Mipmapping example.png|centre|vignette|upright=2|Exemple de mipmapping.]]
Pour faciliter les calculs d'adresse, les LOD d'une même texture sont stockées les uns après les autres en mémoire (dans un tableau, comme diraient les programmeurs). Ainsi, pas besoin de se souvenir de la position en mémoire de chaque LOD : l'adresse de la texture de base, et quelques astuces arithmétiques suffisent. Prenons le cas où la texture de base a une taille L. le premier exemplaire est à l'adresse 0, le second niveau de détail est à l'adresse L, le troisième à l'adresse L + L/4, le suivant à l'adresse L + L/4 + L/16, et ainsi de suite. Le calcul d'adresse demande juste connaître le niveau de détails souhaité et l'adresse de base de la texture. Le niveau de détail voulu est calculé par les pixel shaders, en fonction de la coordonnée de profondeur du pixel à traiter.
Évidemment, cette technique consomme de la mémoire vidéo, vu que chaque texture est dupliquée en plusieurs exemplaires, en plusieurs LOD. Dans le détail, la technique du mip-mapping prend au maximum 33% de mémoire en plus (sans compression). Cela vient du fait qu'en prenant une texture dexu fois plus petite, elle prend 4 fois moins de mémoire : 2 fois moins de pixels en largeur, et 2 fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des LODs, texture de base comprise, prendra X + (X/4) + (X/16) + (X/256) + … Un petit calcul de limite donne 4/3 * X, soit 33% de plus.
===Le cube-mapping===
[[File:Cube mapped reflection example 2.JPG|vignette|Exemple de reflets environnementaux.]]
L''''environnement-mapping''' est une technique de calcul de divers effets graphiques liés à l'environnement, notamment des réflexions. L'idée est de plaquer une texture pré-calculée pour simuler l'effet de l'environnement sur une surface ou un objet 3D. Il en existe plusieurs versions différentes, mais la seule utilisée de nos jours est le ''cube-mapping'', où la texture de l'environnement est plaquée sur un cube, d'où son nom. Le cube en question est utilisé différemment suivant ce que l'on cherche à faire avec le ''cube-mapping''. Les deux utilisations principales sont le rendu du ciel et des décors, et les réflexions sur la surface des objets. Dans les deux cas, l'idée est de précalculer ce que l'on voit du point de vue de la caméra. On place la caméra dans la scène 3D, on place un cube centré sur la caméra, le cube est texturé avec ce que l'on voit de l'environnement depuis la caméra/l'objet de son point de vue.
[[File:Panorama cube map.png|centre|vignette|upright=2|L'illustration montre en premier lieu une ''cubemap'' avec les six faces mises en évidence, puis quel environnement 3D elle permet de simuler, le troisième illustration montrant comment la ''cubemap'' est utilisée pour simuler l'environnement.]]
Le rendu du ciel et des décors lointains dans les jeux vidéo se base sur des '''''skybox''''', à savoir un cube centré sur la caméra, sur lequel on ajoute des textures de ciel ou de décors lointains. Le cube est recouvert par une texture, qui correspond à ce que l'on voit quand on dirige le regard de la caméra vers cette face. Contrairement à ce qu'on pourrait croire, la skybox n'est pas les limites de la scène 3D, les limites du niveau d'un jeu vidéo ou quoique ce soit d'autre de lié à la physique de la scène 3D. La skybox est centrée sur la caméra, elle suit la caméra dans son mouvement. Centrer la skybox sur la caméra permet de simuler des décors très lointains, suffisamment lointain pour qu'on n'ait pas l'illusion de s'en rapprocher en se déplaçant dans la map. De plus, cela évite d'avoir à faire trop de calculs à chaque fois que l'on bouge la caméra. La texture plaquée sur le cube est une texture unique, elle-même découpée en six sous-textures, une par face du cube.
[[File:Skybox example.png|centre|vignette|upright=2|Exemple de Skybox.]]
[[File:Cube mapped reflection example.jpg|vignette|Réflexions calculées par une ''cubemap''.]]
Le ''cube-mapping'' est aussi utilisé pour des reflets. L'idée est de simuler les reflets en plaquant une texture pré-calculée sur l'objet réflecteur. La texture pré-calculée est un dessin de l'environnement qui se reflète sur l'objet, un dessin du reflet à afficher. En la plaquant la texture sur l'objet, on simule ainsi des reflets de l'environnement, mais on ne peut pas calculer d'autres reflets comme les reflets objets mobiles comme les personnages. Et il se trouve que la texture pré-calculée est une ''cubemap''. Pour les environnements ouverts, c'est la ''skybox'' qui est utilisée, ce qui permet de simuler les reflets dans les flaques d'eau ou dans des lacs/océans/autres. Pour les environnements intérieurs, c'est une cubemap spécifique qui utilisée. Par exemple, pour l'intérieur d'une maison, on a une ''cubemap'' par pièce de la maison. Les reflets se calculent en précisant quelle ''cubemap'' appliquer sur l'objet en fonction de la direction du regard.
[[File:Cube map level.png|centre|vignette|Cube map de l'intérieur d'une pièce d'un niveau de jeux vidéo.]]
Toujours est-il que les textures utilisées pour le ''cubemmapping'', appelées des ''cubemaps'', sont en réalité la concaténation de six textures différentes. En mémoire vidéo, la ''cubemap'' est stockée comme six textures les unes à la suite des autres. Lors du rendu, on doit préciser quelle face du cube utiliser, ce qui fait 6 possibilités. On a le même problème qu'avec les niveaux de détail, sauf que ce sont les faces d'une ''cubemap'' qui remplacent les textures de niveaux de détails. L'accès en mémoire doit donc préciser quelle portion de la ''cubemap'' il faut accéder. Et l'accès mémoire se complexifie donc. Surtout que l'accès en question varie beaucoup suivant l'API graphique utilisée, et donc suivant la carte graphique.
Les API 3D assez anciennes ne gérent pas nativement les ''cubemaps'', qui doivent être émulées en logiciel en utilisant six textures différentes. Le pixel shader décide donc quelle ''cubemap'' utiliser, avec quelques calculs sur la direction du regard. L'accès se fait d'une manière assez simple : le shader choisit quelle texture utiliser. Les API 3D récentes gèrent nativement les ''cubemaps''. Dans le cas le plus simple,pour les versions les plus vielles de ces API, les six faces sont numérotées et l'accès à une ''cubemap'' précise quel face utiliser en donnant son numéro. La carte graphique choisit alors automatiquement la bonne texture, mais cela demande de laisser le calcul de la bonne face au pixel shader. D'autres API 3D et cartes graphiques font autrement. Dans les API 3D modenres, les ''cubemap'' sont gérées comme des textures en trois dimensions, adressées avec trois coordonnées u,v,w. La carte graphique utilise ces trois coordonnées de manière à en déduire quelle est la face pertinente, mais aussi les coordonnées u,v dans la texture de la face.
==L'implémentation matérielle du placage de textures==
Pour résumer, la lecture d'un texel demande d'effectuer plusieurs étapes. Dans le cas le plus simple, sans ''mip-mapping'' ou ''cubemapping'', on doit effectuer les étapes suivantes :
* Il faut d'abord normaliser les coordonnées de texture pour qu'elles tombent dans l'intervalle [0,1] en fonction du mode d'adressage désiré.
* Ensuite, les coordonnées u,v doivent être converties en coordonnées entières, ce qui demande une multiplication flottante.
* Enfin, l'adresse finale est calculée à partir des coordonnées entières et en ajoutant l'adresse de base de la texture (et éventuellement avec d'autres calculs arithmétiques suivant le format de la texture).
Tout cela pourrait être fait par le pixel shaders, mais cela implique beaucoup de calculs répétitifs et d'opérations arithmétiques assez lourdes, avec des multiplications flottantes, des additions et des multiplications entières, etc. Faire faire tous ces calculs par les shaders serait couteux en performance, sans compter que les shaders deviendraient plus gros et que cela aurait des conséquences sur le cache d'instruction. De plus, certaines de ces étapes peuvent se faire en parallèle, comme les deux premières, ce qui colle mal avec l'aspect sériel des shaders.
Aussi, les processeurs de shaders incorporent une unité de calcul d'adresse spéciale pour faire ces calculs directement en matériel. L'unité de texture contient au minimum deux circuits : un circuit de calcul d'adresse, et un circuit d'accès à la mémoire. Toute la difficulté tient dans le calcul d'adresse, plus que dans le circuit de lecture. Le calcul d'adresse est conceptuellement réalisé en deux étapes. La première étape qui transforme les coordonnées u,v en coordonnées x,y qui donne le numéro de la ligne et de la colonne du texel dans la texture. La seconde étape prend ces deux coordonnées x,y, l'adresse de la texture, et détermine l'adresse de la tile à lire.
[[File:Unité de texture simple.png|centre|vignette|upright=2|Unité de texture simple]]
===L'implémentation du mip-mapping===
Le ''mip-mapping'' est lui aussi pris en charge par l'unité de calcul d'adresse, car cette technique change l'adresse de base de la texture. La gestion du ''mip-mapping'' est cependant assez complexe. Il est possible de laisser le pixel shader calculer quel niveau de détail utiliser, en fonction de la coordonnée de profondeur z du pixel à afficher. La carte graphique détermine alors automatiquement quelle texture lire, quel niveau de détail, automatiquement. Elle détermine aussi la bonne résolution pour la texture, qui est égal à la résolution de la texture de base, divisée par le niveau de détail. Pour résumer, le niveau de détail est envoyé aux unités de texture, qui s'occupent de calculer l'adresse de base et la résolution adéquates. Quelques calculs arithmétiques simples, donc, qui s'implémentent facilement avec quelques circuits.
Mais une autre méthode laisse la carte graphique déterminer le niveau de détail par elle-même. Dans ce cas, cela demande, outre les deux coordonnées de texture, de calculer la dérivée de ces deux coordonnées dans le sens horizontal et vertical, ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Les quatre dérivées sont les suivantes :
: <math>\frac{du}{dx}</math>, <math>\frac{dv}{dx}</math>, <math>\frac{du}{dy}</math>, <math>\frac{dv}{dy}</math>
Un bon moyen pour obtenir les dérivées demande de regrouper les pixels par groupes de 4 et de faire la différence entre leurs coordonnées de texture respectives. On peut calculer les deux dérivées horizontales en comparant les deux pixels sur la même ligne, et les deux dérivées verticales en comparant les deux pixels sur la même colonne. Mais cela demande de rastériser les pixels par groupes de 4, par ''quads''. Et c'est ce qui est fait sur les cartes graphiques actuelles, qui rastérisent des groupes de 4 pixels à la fois.
[[File:Texture sampler unit with mipmapping.png|centre|vignette|upright=2.0|Unité de texture avec mipmapping.]]
Malheureusement, le calcul exact utilisé pour le choix de la mip-map dépend du GPU considéré et peu de chose est connu quant à ces algorithmes. Il est possible d'inférer le comportement à partir d'observations, mais guère plus. Pour ceux qui veulent en savoir plus, je conseille la lecture de cet article de blog :
* [https://pema.dev/2025/05/09/mipmaps-too-much-detail/ Mipmap selection in too much detail]
===La gestion des accès mémoire===
Enfin, l'unité de texture doit tenir compte du fait que la mémoire vidéo met du temps à lire une texture. En théorie, l'unité de texture ne devrait pas accepter de nouvelle demande de lecture tant que celle en cours n'est pas terminée. Mais faire ainsi demanderait de bloquer tout le pipeline, de l'''input assembler'' au unités de''shaders'', ce qui est tout sauf pratique et nuirait grandement aux performances.
Une solution alternative consiste à mettre en attente les demandes de lectures de texture pendant que la mémoire est occupée. La manière la plus simple d'implémenter des accès mémoire multiples est de les mettre en attente dans une petite mémoire FIFO. Cela implique que les accès mémoire s’exécutent dans l'ordre demandé par le ''shader'' et/ou l'unité de rastérisation, il n'y a pas de réorganisation des accès mémoire ou d’exécution dans le désordre des accès mémoire.
[[File:Texture prefetching.png|centre|vignette|upright=1.5|Accès mémoire simultanés.]]
Évidemment, quand la mémoire FIFO est pleine, le pipeline est alors totalement bloqué. Le rasteriser est prévenu que l'unité de texture ne peut pas accepter de nouvelle lecture de texture. En pratique, la FIFO est généralement d'une taille respectable et permet de mettre en attente beaucoup de demandes de lecture de texture. Il faut de plus noter qu'il y a une FIFO par processeur de ''shader'' sur les cartes graphiques modernes. Quand elle est pleine, le processeur cesse d'exécuter de nouveaux accès mémoire, mais peut continuer à exécuter des ''shaders'' dans les autres unités de calcul, pas besoin de bloquer complétement le pipeline.
===L'intégration du cache de textures===
Il faut noter que les unités de texture incorporent aussi un cache de texture, voire plusieurs. L'intégration des caches de texture avec la mémoire FIFO précédente est quelque peu compliqué, car il faut garantir que les lectures de texture se fassent dans le bon ordre. On ne peut pas exécuter une lecture dans le cache alors que des lectures précédentes sont en attente de lecture en mémoire vidéo. Et cela pose un gros problème : une lecture dans le cache de texture prend quelques dizaines de cycles d'horloge, alors qu'une lecture en mémoire vidéo en prend facilement 400 à 800 cycles, parfois plus. Et cela fait que l'ordre des accès mémoire peut s'inverser.
Prenons par exemple un accès au cache précédé et suivi par deux accès en mémoire vidéo. Le premier démarre au cycle 1, et se termine au cycle numéro 400. L'accès au cache commence au cycle 2 et se termine 20 cycles après, au cycle numéro 22. En clair, la lecture dans le cache s'est terminée avant l'accès mémoire qui le précède. Les textures ne sont donc plus lues dans l'ordre. Et il faut trouver une solution pour éviter cela.
La solution est de retarder les lectures dans le cache tant que tous les accès précédents ne sont pas terminés. Mais pour retarder les lectures en question, il faut d'abord savoir si la lecture atterrit dans le cache ou non, ce qui demande d'accéder au cache. On fait face à un dilemme : on veut retarder les accès au cache, mais les différencier des lectures déclenchant des accès mémoire demande d'accéder au cache en premier lieu. La solution est décrite dans l'article "Prefetching in a Texture Cache Architecture" par Igehy et ses collègues. Elle se base sur deux idées combinées ensemble.
La première idée est de séparer l'accès au cache en deux : une étape qui vérifie si les texels à lire sont dans le cache, et une étape qui accède aux données dans le cache lui-même. Un cache de texture est donc composé de deux circuits principaux. Le premier vérifie la présence des texels dans le cache. Il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Ensuite, en plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
La seconde idée est de retarder l'accès au cache entre les deux phases. La première étape d'un accès mémoire vérifie si la donnée est dans le cache ou non. Puis, on retarde la lecture des données, pour attendre que toutes les lectures précédentes soient terminées. Et enfin, troisième étape : la lecture des texels dans la mémoire cache proprement dite. Les accès mémoire passant par la mémoire vidéo se font de la même manière, à une différence près : la lecture dans le cache est remplacée par la lecture en mémoire vidéo. Tout démarre avec une demande à l'unité de tags, qui vérifie si le texel est dans le cache ou non. Puis on retarde l'accès tant que la mémoire vidéo est occupée, puis on effectue la lecture en mémoire vidéo.
Si ce n'est pas le cas, l'accès mémoire est envoyé à la mémoire vidéo comme précédemment, à savoir qu'il est mis en attente dans une mémoire FIFO, puis envoyé à la mémoire vidéo dès que celle-ci est libre. Mais en sortie de la mémoire, la donnée lue est envoyée dans le cache de texture, par dans l'unité de filtrage. Pour savoir où placer la donnée lue, l'unité de tag a réservé une ligne de cache précise, une adresse bien précise. L'adresse en question est disponible en lisant une autre mémoire FIFO, qui a mis en attente l'adresse en question, en attendant que l'accès mémoire se termine. La donnée est alors écrite dans le cache, puis lue par l'unité de filtrage de textures.
Pour une lecture dans le cache, le déroulement est similaire, mais sans le passage par la mémoire. La lecture fait une demande à l'unité de tag, et celle-ci répond que la donnée est bien dans le cache. Elle place alors l'adresse à lire dans la file d'attente. Une fois que les accès mémoire précédents sont terminés, l'adresse sort de la file d'attente et est envoyée à la mémoire de données. La lecture s'effectue, les texels sont envoyés à l'unité de filtrage de textures. La seule différence avec un ''phased cache'' normal est l'insertion de l'adresse à lire dans une FIFO qui vise à mettre en attente
[[File:Unité de texture avec un cache de texture.png|centre|vignette|upright=2.0|Unité de texture avec un cache de texture]]
Pour résumer, l'implémentation précédente garantit une exécution des lectures dans leur ordre d'arrivée. Et pour cela, elle retarde les lectures dans le cache tant que les lectures en mémoire précédentes ne sont pas terminées. L'accès au cache est plus rapide que l'accès en mémoire vidéo, mais le retard ajouté pour garantir l'ordre des lectures fait que le temps d'accès est très long.
==Le filtrage de textures==
Plaquer des textures sans autre forme de procès ne suffit pas à garantir des graphismes d'une qualité époustouflante. La raison est que les sommets et les texels ne tombent pas tout pile sur un pixel de l'écran : le sommet associé au texel peut être un petit peu trop en haut, ou trop à gauche, etc. Une explication plus concrète fait intervenir les coordonnées de texture. Souvenez-vous que lorsque l'on traduit une coordonnée de texture u,v en coordonnées x,y, on obtient un résultat qui ne tombe pas forcément juste. Souvent, le résultat a une partie fractionnaire. Si celle-ci est non-nulle, cela signifie que le texel/sommet n'est pas situé exactement sur le pixel voulu et que celui-ci est situé à une certaine distance. Concrètement, le pixel tombe entre quatre texels, comme indiqué ci-dessous.
[[File:Filtrage texture.png|centre|vignette|upright=2.0|Position du pixel par rapport aux texels.]]
Pour résoudre ce problème, on doit utiliser différentes techniques d'interpolation, aussi appelées techniques de '''filtrage de texture''', qui visent à calculer la couleur du pixel final en fonction des texels qui l'entourent. Il existe de nombreux types de filtrage de textures, qu'il s'agisse du filtrage linéaire, bilinéaire, trilinéaire, anisotropique et bien d'autres.
Tous ont besoin d'avoir certaines informations qui sont généralement fournies par les circuits de calcul d'adresse. La première est clairement la partie fractionnaire des coordonnées x,y. La seconde est la dérivée de ces deux coordonnées dans le sens horizontal et vertical., ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Toujours est-il que le filtrage de texture est une opération assez lourde, qui demande beaucoup de calculs arithmétiques. On pourrait en théorie le faire dans les pixels shaders, mais le cout en performance serait absolument insoutenable. Aussi, les cartes graphiques intègrent toutes un circuit dédié au filtrage de texture, le ''texture sampler''. Même les plus anciennes cartes graphiques incorporent une unité de filtrage de texture, ce qui nous montre à quel point cette opération est importante.
[[File:Texture unit.png|centre|vignette|upright=2.0|Unité de texture.]]
On peut configurer la carte graphique de manière à ce qu'elle fasse soit du filtrage bilinéaire, soit du filtrage trilinéaire, on peut configurer le niveau de filtrage anisotropique, etc. Cela peut se faire dans les options de la carte graphique, mais cela peut aussi être géré par l'application. La majorité des jeux vidéos permettent de régler cela dans les options. Ces réglages ne concernent pas la texture elle-même, mais plutôt la manière dont l'unité de texture doit fonctionner. Ces réglages sur l''''état de l'unité de texture''' sont mémorisés quelque part, soit dans l'unité de texture elle-même, soit fournies avec la ressource de texture elle-même, tout dépend de la carte graphique. Certaines cartes graphiques mémorisent ces réglages dans les unités de texture ou dans le processeur de commande, et tout changement demande alors de réinitialiser l'état des unités de texture, ce qui prend un peu de temps. D'autres placent ces réglages dans les ressources de texture elles-mêmes, ce qui rend les modifications de configuration plus rapides, mais demande plus de circuits. D'autres cartes graphiques mélangent les deux options, certains réglages étant globaux, d'autres transmis avec la texture. Bref, difficile de faire des généralités, tout dépend du matériel et le pilote de la carte graphique cache tout cela sous le tapis.
Maintenant que cela est dit, voyons quelles sont les différentes méthodes de filtrage de texture et comment la carte graphique fait pour les calculer.
===Le filtrage au plus proche===
La méthode de filtrage la plus simple consiste à colorier avec le texel le plus proche. Cela revient tout simplement à ne pas tenir compte de la partie fractionnaire des coordonnées x,y, ce qui est très simple à implémenter en matériel. C'est ce que l'on appelle le '''filtrage au plus proche''', aussi appelé ''nearest filtering''.
Autant être franc, le résultat est assez pixelisé et peu agréable à l’œil. Par contre, le résultat est très rapide à calculer, vu qu'il ne demande aucun calcul à proprement parler. Elle ne fait pas appel à la parti fractionnaire des coordonnées entières de texture, ni aux dérivées de ces coordonnées. On peut combiner cette technique avec le mip-mapping, ce qui donne un résultat bien meilleur, bien que loin d'être satisfaisant. Au passage, toutes les techniques de filtrage de texture peuvent se combiner avec du mip-mapping, certaines ne pouvant pas faire sans.
[[File:Interpolation-nearest.svg|centre|vignette|Filtrage de texture au plus proche.]]
===Le filtrage linéaire===
Le filtrage le plus simple est le '''filtrage linéaire'''. Il effectue une interpolation linéaire entre deux mip-maps, deux niveaux de détails. Pour comprendre l'idée, nous allons prendre une situation très simple, avec une texture carrée de 512 texels de côté. Le mip-mapping crée plusieurs textures : une de 256 texels de côté, une de 128 texels, une de 64, etc. Maintenant, la texture est sur un objet à une certaine distance de l'écran, vu de face. Le résultat est qu'elle correspond à l'écran à un carré de 300 pixels de côté (pas d'erreur : pixels, pas texels). Dans ce cas, la texture se trouve entre deux mip-maps : celle de 512 pixels de côté, celle de 256. Laquelle choisir ? Le filtrage au plus proche prend la texture de 512 pixels de côté. Le filtrage linéaire lui, fait autrement.
Vu que la texture est entre deux mip-maps, l'idée est de prendre le texel au plus proche dans chaque texture et de faire une sorte de moyenne appelée l'interpolation linéaire. L'interpolation par du principe que la couleur varie entre les deux texels en suivant une fonction affine, illustrée ci-dessous. Ce ne serait évidemment pas le cas dans le monde réel, mais on supposer cela donne une bonne approximation de ce à quoi ressemblerait une texture à plus haute résolution. On peut alors calculer la couleur du pixel par une simple moyenne pondérée par la distance. Le résultat est que les transitions entre deux niveaux de détails sont plus lisses, moins abruptes.
[[File:Lin interp -é.png|centre|vignette|upright=2.0|Interpolation linéaire.]]
===Le filtrage bilinéaire===
Le filtrage bilinéaire effectue une sorte de moyenne pondérée des quatre texels les plus proches du pixel à afficher. Pour cela, rappelez-vous ce qui a été dit plus haut : les coordonnées x,y d'un pixel ont une partie entière et une partie fractionnaire. Le filtrage au plus proche élimine les parties fractionnaires, ce qui donne une coordonnée x,y. Avec le filtrage bilinéaire, on prend les texels de coordonnées (x,y) ; (x+1,y) ; (x,y+1) ; (x+1,y+1), le pixel étant entre ces 4 texels.
Mais le filtrage ne fait pas qu'une simple moyenne, il prend en compte les parties fractionnaires pour faire la moyenne. En effet, le pixel n'est pas au milieu du carré de texel, il est quelque part mais est souvent plus proche d'un texel que des autres. Et il faut donc pondérer la moyenne par les distances aux 4 texels. Pour cela, la moyenne est calculée à partir d'interpolations linéaires. Avec 4 pixels, nous allons devoir calculer la couleur de deux points intermédiaires. La couleur de ces deux points se calcule par interpolation linéaire, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.
[[File:Bilin3.png|centre|vignette|upright=2|Filtrage bilinéaire de texture.]]
Le circuit qui permet de faire l'interpolation bilinéaire est particulièrement simple. On trouve un circuit de chaque pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu, et un pour la transparence. Chacun de ces circuit est composé de sous-circuits chargés d'effectuer une interpolation linéaire, reliés comme suit.
[[File:Texture sampler unit.png|centre|vignette|Unité de filtrage bilinéaire.]]
Vous noterez que le filtrage bilinéaire accède à 4 pixels en même temps. Fort heureusement, les textures sont stockées de manière à ce qu'on puisse charger les 4 pixels en une fois, comme on l'a vu plus haut. Le filtrage bilinéaire a de fortes chances que les 4 pixels filtrés soient dans la même ''tile'', la seule exception étant quand ils sont tout juste sur le bord d'une ''tile''.
: La console de jeu Nintendo 64 n'utilise que trois pixels au lieu de quatre dans son interpolation bilinéaire, qui en devient une interpolation quasi-bilinéaire. La raison derrière ce choix est une question de performances, comme beaucoup de décisions de ce genre. Le résultat est un rendu imparfait de certaines textures.
===Le filtrage trilinéaire===
Avec le filtrage bilinéaire, des discontinuités apparaissent sur certaines surfaces. Par exemple, pensez à une texture de sol : elle est appliquée plusieurs fois sur toute la surface du sol. A une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512*512 à 256*256, ce qui est visible pour un joueur attentif. De telles transitions sont lissées grâce au filtrage linéaire, il n'y a plus qu'à le combiner avec le filtrage bilinéaire. Rien d’incompatible : le premier filtre l'intérieur d'une mip-map, le second combine deux mip-maps.
Le filtrage trilinéaire prend les deux mip-maps les plus proches, fait un filtrage bilinéaire avec chacune, puis fait une « une moyenne » pondérée entre les deux résultats. Le circuit de filtrage trilinéaire existe en plusieurs versions. La plus simple, illustrée ci-dessous, effectue deux filtrages bilinéaires en parallèle, dans deux circuits séparés, puis combine leurs résultats avec un circuit d'interpolation linéaire. Mais ce circuit nécessite de charger 8 texels simultanément. Qui plus est, ces 8 texels ne sont pas consécutifs en mémoire, car ils sont dans deux niveaux de détails/mip-maps différents.
[[File:Parallel trilinear filtering.png|centre|vignette|upright=2.0|Unité de filtrage trilinéaire parallèle.]]
Vu qu'on lit des texels dans deux mip-maps, les texels sont lus en deux fois : 4 texels provenant de la première mip-map, suivis par les 4 texels de l'autre mip-map. Les 4 premiers texels doivent donc être mis en attente dans des registres, en attendant que les 4 autres arrivent. Une amélioration du circuit précédent gère cela en ajoutant des registres. Il lit les 4 premiers texels, les filtre avec une interpolation bilinéaire, et mémorise le résultat dans un registre. Puis, il lit les 4 autres texels, les filtre, et met le résultat dans un second registre. A ce moment là, un circuit d'interpolation linéaire finit le travail. On économise donc un circuit d'interpolation bilinéaire, sans que les performances soient trop impactées.
[[File:Filtrage trilineaire.png|centre|vignette|upright=1.0|Unité de filtrage trilineaire série.]]
Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le temps où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. À chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients, et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.
===Le filtrage anisotrope===
D'autres artefacts peuvent survenir lors de l'application d'une texture, la perspective pouvant déformer les textures et entraîner l'apparition de flou. La raison à cela est que les techniques de filtrage de texture précédentes partent du principe que la texture est vue de face. Prenez une texture carrée, par exemple. Vue de face, elle ressemble à un carré sur l'écran. Mais tournez la caméra, de manière à voir la texture de biais, avec un angle, et vous verrez que la forme de la texture sur l'écran est un trapèze, pas un carré. Cette déformation liée à la perspective n'est pas prise en compte par les méthodes de filtrage de texture précédentes. Pour le dire autrement, les techniques de filtrage précédentes partent du principe que les 4 texels qui entourent un pixel forment un carré, ce qui est vrai si la texture est vue de face, sans angle, mais ne l'est pas si la texture n'est pas perpendiculaire à l'axe de la caméra. Du point de vue de la caméra, les 4 texels forment un trapèze d'autant moins proche d'un carré que l'angle est grand.
Pour corriger cela, les chercheurs ont inventé le '''filtrage anisotrope'''. En fait, je devrais plutôt dire : LES filtrages anisotropes. Il en existe un grand nombre, dont certains ne sont pas utilisés dans les cartes graphiques actuelles, soit car ils trop gourmand en accès mémoires et en calculs pour être efficaces, soit car ils ne sont pas pratiques à mettre en œuvre. Il est très difficile de savoir quelles sont les techniques de filtrage de texture utilisées par les cartes graphiques, qu'elles soient récentes ou anciennes. Beaucoup de ces technologies sont brevetées ou gardées secrètes, et il faudrait vraiment creuser les brevets déposés par les fabricants de GPU pour en savoir plus. Les algorithmes en question seraient de plus difficiles à comprendre, les méthodes mathématiques cachées derrière ces méthodes de filtrage n'étant pas des plus simple.
[[File:Anisotropic filtering en.png|centre|vignette|upright=2|Exemple de filtrage anisotrope.]]
==La compression de textures==
Les textures les plus grosses peuvent aller jusqu'au mébioctet, ce qui est beaucoup. Pour limiter la casse, les textures sont compressées. La '''compression de texture''' réduit la taille des textures, ce qui peut se faire avec ou sans perte de qualité. Elle entraîne souvent une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande. Mais il s'agit là d'une technique très simple, beaucoup plus simple que les techniques que nous allons voir dans cette section. Nous allons voir quelque algorithmes de compression de textures de complexité intermédiaire, mais n'allons pas voir l'état de l'art. Il existe des formats de texture plus récents que ceux qui nous allons aborder, comme l{{'}}''Ericsson Texture Compression'' ou l{{'}}''Adaptive Scalable Texture Compression'', plus complexes et plus efficaces.
Notons que les textures sont compressées dans les fichiers du jeu, mais aussi en mémoire vidéo. Les textures sont décompressées lors de la lecture. Pour cela, la carte graphique contient alors un circuit, capable de décompresser les textures lorsqu'on les lit en mémoire vidéo. Les cartes graphiques supportent un grand nombre de formats de textures, au niveau du circuit de décompression. Du fait que les textures sont décompressées à la volée, les techniques de compression utilisées sont assez particulières. La carte graphique ne peut pas décompresser une texture entière avant de pouvoir l'utiliser dans un ''pixel shader''. A la place, on doit pouvoir lire un morceau de texture, et le décompresser à la volée. On ne peut utiliser les méthodes de compression du JPEG, ou d'autres formats de compression d'image. Ces dernières ne permettent pas de décompresser une image morceau par morceau.
Pour permettre une décompression/compression à la volée, les textures sont des textures tilées, généralement découpées en tiles de 4 * 4 texels. Les ''tiles'' sont compressées indépendamment les unes des autres. Et surtout, avec ou sans compression, la position des tiles en mémoire ne change pas. On trouve toujours une tile tous les T octets, peu importe que la tile soit compressée ou non. Par contre, une tile compressée n'occupera pas T octets, mais moins, là où une tile compressée occupera la totalité des T octets. En clair, compresser une tile fait qu'il y a des vides entre deux tiles dans al mémoire vidéo, mais ne change rien à leur place en mémoire vidéo qui est prédéterminée, peu importe que la texture soit compressée ou non. L'intérêt de la compression de textures n'est pas de réduire la taille de la texture en mémoire vidéo, mais de réduire la quantité de données à lire/écrire en mémoire vidéo. Au lieu de lire T octets pour une tile non-compressée, on pourra en lire moins.
===La palette indicée et la technique de ''Vector quantization''===
La technique de compression des textures la plus simple est celle de la '''palette indicée''', que l'on a entraperçue dans le chapitre sur les cartes d'affichage. La technique de '''''vector quantization''''' peut être vue comme une amélioration de la palette, qui travaille non pas sur des texels, mais sur des ''tiles''. À l'intérieur de la carte graphique, on trouve une table qui stocke toutes les ''tiles'' possibles. Chaque ''tile'' se voit attribuer un numéro, et la texture sera composé d'une suite de ces numéros. Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l’embarqué utilisent ce genre de compression.
===Les algorithmes de ''Block Truncation coding''===
La première technique de compression élaborée est celle du '''''Block Truncation Coding''''', qui ne marche que pour les images en niveaux de gris. Le BTC ne mémorise que deux niveaux de gris par ''tile'', que nous appellerons couleur 1 et couleur 2, les deux niveaux de gris n'étant pas le même d'une ''tile'' à l'autre. Chaque pixel d'une ''tile'' est obligatoirement colorié avec un de ces niveaux de gris. Pour chaque pixel d'une ''tile'', on mémorise sa couleur avec un bit : 0 pour couleur 1, et 1 pour couleur 2. Chaque ''tile'' est donc codée par deux entiers, qui codent chacun un niveau de gris, et une suite de bits pour les pixels proprement dit. Le circuit de décompression est alors vraiment très simple, comme illustré ci-dessous.
[[File:Block Truncation coding.jpg|centre|vignette|upright=2.0|Block Truncation coding.]]
La technique du BTC peut être appliquée non pas du des niveaux de gris, mais pour chaque composante Rouge, Vert et Bleu. Dans ces conditions, chaque ''tile'' est séparée en trois sous-''tiles'' : un sous-bloc pour la composante verte, un autre pour le rouge, et un dernier pour le bleu. Cela prend donc trois fois plus de place en mémoire que le BTC pur, mais cela permet de gérer les images couleur.
===Le format de compression S3TC / DXTC===
L'algorithme de '''Color Cell Compression''', ou CCC, améliore le BTC pour qu'il gère des couleurs autre que des niveaux de gris. Ce CCC remplace les deux niveaux de gris par deux couleurs. Une ''tile'' est donc codée avec un entier 32 bits par couleur, et une suite de bits pour les pixels. Le circuit de décompression est identique à celui utilisé pour le BTC.
[[File:Color Cell Compression.jpg|centre|vignette|Color Cell Compression.]]
[[File:Dxt1-memory-layout.png|vignette|Dxt1 et ''color cell compression''.]]
Le format de compression de texture utilisé de base par Direct X, le DXTC, est une version amliorée de l'algorithme précédent. Il est décliné en plusieurs versions : DXTC1, DXTC2, etc. La première version du DXTC est une sorte d'amélioration du CCC : il ajoute une gestion minimale de transparence, et découpe la texture à compresser en ''tiles'' de 4 pixels de côté. La différence, c'est que la couleur finale d'un texel est un mélange des deux couleurs attribuée au bloc. Pour indiquer comment faire ce mélange, on trouve deux bits de contrôle par texel.
Si jamais la couleur 1 < couleur2, ces deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (2 * Couleur1 + Couleur2) / 3
* 11 = (Couleur1 + 2 * Couleur2) / 3
Sinon, les deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (Couleur1 + Couleur2) / 2
* 11 = Transparent
[[File:DXTC.jpg|centre|vignette|DXTC.]]
Le circuit de décompression du DXTC ressemble alors à ceci :
[[File:Circuit de décompression du DXTC.jpg|centre|vignette|upright=2.0|Circuit de décompression du DXTC.]]
===Les format DXTC 2, 3, 4 et 5 : l'ajout de la transparence===
Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3, lui-même replacé par le DXT4 et par le DXT5. Dans le DXT3, la transparence fait son apparition. Pour cela, on ajoute 64 bits par ''tile'' pour stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1.
[[File:Dxt23-memory-layout.png|centre|vignette|Dxt 2 et 3.]]
Dans le DXT4 et le DXT5, la méthode utilisée pour compresser les couleurs l'est aussi pour les valeurs de transparence. L'information de transparence est stockée par un en-tête contenant deux valeurs de transparence, le tout suivi d'une matrice qui attribue trois bits à chaque texel. En fonction de la valeur des trois bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1.
[[File:Dxt45-memory-layout.png|centre|vignette|Dxt 4 et 5.]]
===Le format de compression PVRTC===
Passons maintenant à un format de compression de texture un peu moins connu, mais pourtant omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle travaille surtout dans les cartes graphiques embarquées. Ses cartes se trouvent notamment dans l'ipad, l'iPhone, et bien d'autres smartphones actuels.
Avec le PVRTC, les textures sont encore une fois découpées en ''tiles'' de 4 texels par 4, mais la ressemblance avec le DXTC s’arrête là. Chacque ''tile'' est codée avec :
* une couleur codée sur 16 bits ;
* une couleur codée sur 15 bits ;
* 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
* et un bit de modulation, qui permet de configurer l’interprétation des bits de mélange.
Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4*4.
==Annexe : le ''normal-mapping'' hardware==
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, parlons un peu du ''normal mapping'' et de son implémentation dans les unités de texture. Pour rappel, 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. Et pour comprendre quel est le rapport avec les textures, nous allons devoir faire quelques rappels sur l'éclairage par pixel.
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
L'éclairage se fait en utilisant de nombreux calculs, qu'on a détaillé dans le chapitre sur les bases du rendu 3D. Ceux-ci utilisent la normale d'un sommet, à savoir un vecteur orienté à la verticale de la surface. En théorie, il y a une normale par sommet, ce qui fait que les calculs d'éclairage doivent se faire au niveau géométrique, avant la rastérisation. Mais pour obtenir un éclairage de meilleure qualité, il y a des techniques qui calcule l'éclairage d'une scène 3D pixel par pixel.
Une première technique d'éclairage par pixel interpole les normales lors de l'étape de rastérisation. On obtient alors un éclairage de Phong. Une autre solution est celle du ''normal mapping''. Le '''''normal mapping''''' précalcule les normales d'une surface dans une texture, appelée la ''normal map''. L'éclairage est alors réalisé par un pixel shader, qui lit les normales depuis cette texture et fait les calculs d'éclairage avec.
===L'usage des textures usuelles pour les ''normal maps''===
La ''normal map'' est une texture, donc. Mais ce n'est pas une texture comme une autre. Elle mémorise un vecteur pour chaque texel, pas une couleur. Il est possible d'utiliser les formats de textures vus plus haut, même si cela ne donne pas de bons résultats. Après tout, un vecteur est codé par trois coordonnées x,y,z, il est possible de coder chacune avec un octet et de packager cela dans une texture RGB classique. La coordonnée x va dans la composante Rouge, la coordonnée Y dans la composante Bleu, et la coordonnée z dans la composante z.
Une première optimisation est de ne pas mémoriser les trois coordonnées, mais seulement deux d'entre elles et de calculer la troisième. En effet, les normales sont des vecteurs ''normalisés'', c'est à dire que leur longueur vaut 1, par construction. Vu que la taille est connue à l'avance, on peut en déduire la coordonnée z à partir des coordonnées x et y, avec l'usage du théorème de Pythagore. Par définition, <math>z^2 = 1 - x^2 - y^2</math>. Le calcul peut être fait dans le ''pixel shader'' sans problème.
: Pour être précis, il faut que les normales soient définies d'une certaine manière pour que ça marche. Les normales ne sont pas définies dans le même système de coordonnées que le modèle 3D, mais dans un autre système appelé l'''espace tangent''.
Notons que l'accès à la ''normal map'' se fait comme pour n'importe quelle texture : les texels sont lus en mémoire vidéo, puis le filtrage de texture est appliqué, et enfin le tout est envoyé au ''pixel shader''. En théorie, le filtrage effectue une sorte d'interpolation des normales automatique, ce qui permet de trouver la normale à un pixel précis, même s'il n'est pas sur un texel.
===Le format de texture 3Dc===
Le problème est que le rendu final n'est pas très beau. Et les opérations de filtrage ne donnent pas de très bons résultats. Pour remédier à ces problèmes, des formats de texture spécialisés pour les ''normal map'' ont été inventés. Le premier d'entre eux est le '''3Dc''', aussi appelé BC5 pour ''Block Compression 5''. Il utilise un octet par texel, au lieu de trois avec une texture RGB normale non-compressée.
Avec le 3Dc, seules coordonnées sont mémorisées, la troisième est calculée, comme dit plus haut. Pour le reste, la compression de la ''normal map'' ressemble un peu à celle des textures. La ''normal map'' est découpée en blocs de 4 par 4 texels de côté, chacune étant encodée en tenant compte de certaines redondances. Dans un bloc, les coordonnées x sont dans un certain intervalle, allant de <math>x_{min}</math> à <math>x_{max}</math>, idem pour la coordonnée y qui est dans l'intervalle <math>y_{min}</math>, <math>y_{max}</math>. L'intervalle <math>x_{min}</math> à <math>x_{max}</math> est coupé en 8 parts égales, ce qui fait 8 valeurs possibles dans cet intervalle, chacune correspondant à une valeur x possible pour une normale du bloc. La normale est donc encodée en précisant quelle valeur est la bonne, en utilisant un indice de 3 bits pour cela. La valeur exacte de la coordonnée x se calcule à partir de l'indice comme suit :
: <math>x = x_{min} + \left( \frac{x_{min} - x_{min}}{8} \right) \times \text{indice} </math>
Pour encoder un bloc de 4 par 4 normales, il faut donc :
* Un octet pour chaque coordonnée <math>x_{min}</math>, <math>x_{max}</math>, <math>y_{min}</math>, <math>y_{max}</math>.
* 6 bits par normale : 3 pour encoder le décalage de la coordonnée x, 3 pour la coordonnée y.
Le tout permet d'encoder un bloc de 16 normales en seulement 128 bits, soit un octet par normale. Le ''pixel shader'' se débrouille pour décompresser un bloc. Il effectue notamment le calcul du dessus, pour retrouver la coordonnée x. Les calculs étant particulièrement simples, le cout en performance est très faible. Et ce d'autant plus qu'ils demandent juste de faire des additions et des multiplications entières, que l'unité scalaire peut faire en parallèle d'autres opérations plus gourmandes. Et le cout en calculs est de toute façon compensé par l'économie de mémoire et de bande passante lié à la compression : diviser la taille des données par trois, ca a un sacré impact !
==Annexe : les ''shadowmap'' hardware==
Les anciens GPU, notamment la Geforce FX, avaient des fonctionnalités spécifiques pour le calcul des ombres. Dans la plupart des jeux vidéos de l'époque, et même de maintenant, les ombres sont calculées avec la technique des ''shadowmap''. L'idée est assez simple sur le principe : un pixel est dans l'ombre quand il est invisible depuis une source de lumière.
L'idée est que le rendu est réalisé en plusieurs passes, avec une passe par source de lumière et une passe finale pour calculer l'image finale. Nous allons expliquer la technique avec une seule source de lumière, et allons utiliser l'exemple de la scène ci-dessous.
[[File:7fin.png|centre|vignette|Scène 3D d'exemple.]]
===La technique du ''shadowmapping''===
[[File:2shadowmap.png|vignette|Résultat de la première passe : ''shadowmap''..]]
La première passe rend l'image depuis le point de vue de la source de lumière. Cette première passe ne rend pas les couleurs de la scène, elle ne s'intéresse qu'à la profondeur des pixels. Le résultat est que l'image ne rend que le tampon de profondeur. Celui-ci est ensuite réutilisé comme texture pour la passe suivante. La texture en question est appelée la '''''shadownmap'''''.
La perspective utilisée, ainsi que le ''view frustrum'', dépend de la source de lumière. Pour une source de lumière qui émet un cône de lumière, le ''view frustrum'' de l'image rendue doit contenir tout le cone de lumière, et doit coller le plus possible à celui-ci. Pour une source directionnelle, comme le soleil, une perspective orthographique est utilisée.
La seconde passe rend l'image du point de vue de la caméra, pour rendre l'image finale. Elle rend l'image finale, qui est composée de pixels, chacun ayant une position à l'écran x,y, et une profondeur z. Les coordonnées sont transformées pour obtenir la position de ce pixel depuis le point de vue de la caméra. Une simple multiplication de matrice suffit, rien de bien compliqué, un shader peut le faire.
[[File:5failed.png|vignette|Résultat du test des comparaisons.]]
Après cette étape, on a alors les coordonnées x,y,z de ce pixel du point de vue de la caméra, et la ''shadowmap''. Il est alors possible d'accéder à la ''shadowmap'' au même endroit, à la même place que le pixel testé, aux mêmes coordonnées x,y. Si la profondeur du pixel est supérieure à celle de la shadowmap au même endroit, alors le pixel est situé derrière la surface visible, donc est dans l'ombre. Sinon, il n'est pas dans l'ombre. Le même procédé est répété sur chaque pixel de l'écran.
===Les optimisations hardware du ''shadowmapping''===
La technique des ''shadowmap'' demande donc de calculer une texture ''shadowmap'', puis de lire celle-ci et de faire des comparaisons de profondeur. Les GPU comme la Geforce FX intégraient du matériel dans les unités de texture pour faciliter ce travail. Les unités de texture pouvaient lire les ''shadowmap'', et faire la comparaison de profondeur toutes seules, elles avaient des circuits pour. Il suffisait de leur fournir le pixel à tester, ses coordonnées x,y,z, et l'adresse de la ''shadowmap''. Les unités de texture renvoyaient alors un résultat valant 0 ou 1 : 1 si le pixel est dans l'ombre, 0 sinon.
Elles pouvaient même effectuer du filtrage de texture sur les ''shadowmap''. Mais le filtrage était différent de celui utilisé sur les autres textures : moyenner des valeurs de profondeur ne marche pas bien. Elles utilisaient des techniques de filtrage différentes : elles faisaient les tests de comparaison, puis faisaient la moyenne des résultats. Ainsi, pour du filtrage bilinéaire, elles lisaient 4 texels dans la ''shadowmap'', puis faisaient 4 tests de comparaison, et moyennaient les 4 résultats.
==Annexe : les textures virtuelles==
Les '''textures virtuelles''' sont une optimisation des textures normales, qui visent à accélérer le rendu de terrains de grande taille. Imaginez par exemple un monde assez ouvert, comme un environnement en forêt ou en montagne, avec une grande distance de visibilité. Avec de tels terrains, le "sol" est recouvert par une texture de sol unique qui recouvre tout le terrain. Elle ne se répète pas, est de très grande taille, et peut parfois recouvrir toute la map ! Mais il n'y a pas assez de mémoire vidéo pour mémoriser la texture toute entière. La seule solution est la suivante : une partie de la texture est placée en mémoire vidéo, le reste est soit placé en mémoire RAM ou sur le disque dur.
Pour cela, le moteur de jeu utilise une optimisation ingénieuse, basée sur une observation assez basique : une bonne partie de la texture est visible, mais le reste est caché par des arbres, des habitations ou d'autres obstacles. Une optimisation possible de ne garder en mémoire vidéo que les portions visibles de la texture, pas les portions cachées. Une autre optimisation mélange textures virtuelles et ''mip-mapping''. L'idée est que pour les portions lointaines d'une texture, la texture utilisée est une ''mip-map'' de basse résolution. L'idée est alors de ne charger que la ''mip-map'' adéquate, pas les autres niveaux de détail. En clair, la texture de base n'est pas chargée en mémoire vidéo, mais la ''mip-map'' basse résolution l'est.
===Une texture à deux niveaux===
L'implémentation des textures virtuelles découpe les méga-textures en ''tiles'', en morceaux rectangulaires de taille modeste. En clair, le terrain est découpé en morceau rectangulaires/carrés. Seules les tiles nécessaires sont chargées en mémoire vidéo, pas les autres. Par exemple, les ''tiles'' non-visibles ne sont pas placées en mémoire vidéo, seules les ''tiles'' visibles le sont. De même, il y a une ''tile'' par niveau de mip-map : seul la tile correspondant le niveau adéquat est en mémoire vidéo, les autres niveaux de détail ne sont pas chargés. On peut faire une analogie avec la mémoire virtuelle, où les données sont découpées en pages, qui sont chargées en mémoire RAM à la demande, suivant les besoins, les données pouvant être swappées sur le disque dur si elles sont peu utilisées. Sauf qu'ici, il s'agit de textures qui sont découpées en pages chargées à la demande en mémoire vidéo, depuis la RAM système.
Une texture virtuelle est en réalité un système à deux niveaux : une liste de ''tiles'' et les ''tiles'' elles-mêmes. La liste de ''tiles'' est appelée un '''atlas de texture''', c'est un peu l'équivalent de la ''tilemap'' pour le rendu 2D. Rendre une texture demande de calculer quelle ''tile'' contient le texel à afficher, consulter la ''tile'' en question, puis récupérer le texel adéquat dans cette ''tile''. La ''tile'' est donc une texture, mais la texture à charger est choisie parmi un ensemble, qui est ici l'atlas de texture.
===L'implémentation : logicielle versus matérielle===
Les textures virtuelles ont été utilisées pour la première fois par les jeux Rage 1 et 2 d'IdSoftware, et quelques jeux ultérieurs comme DOOM 2016. IdSoftware les appelait des '''''mega-textures'''''. L'optimisation permettait des gains en performance assez impressionnants. Le jeu Rage 1 utilisait une texture carrée unique de 128k pixels de côté pour rendre le terrain. En théorie, une telle texture devrait prendre 64 giga-octets, mais le jeu tournait correctement avec 512 méga-octets de RAM, poussivement avec seulement 256 méga-octets de RAM.
De nos jours, les textures virtuelles sont supportées par beaucoup de jeux vidéos, les moteurs les plus courants gèrent de telles textures de manière logicielles. Mais quelques GPU récents supportent les textures virtuelles. Sur les GPU récents, l'atlas de texture est géré nativement par le matériel. Le GPU choisit quelle ''tile'', quelle texture choisir pour rendre le texel adéquat. Pour cela, le GPU calcule quelle ''tile'' charger, consulte l'atlas de texture, et lit la texture de ''tile'' adéquate.
Mais l'implémentation sur les GPU récents a de nombreuses limitations. La limitation la plus importante est que la taille des textures virtuelles ne peut pas dépasser la taille d'une texture normale, soit 32768 pixels de côté pour une texture carrée environ sur les GPU de 2020. De plus, le chargement d'une ''tile'' est très lent. En clair, dès qu'on veut changer de niveau de mip-map pour une tile, ou dès qu'une tile devient visible, le chargement de la tile peut facilement prendre plusieurs centaines de millisecondes. Le filtrage de texture est très complexe avec des textures virtuelles, ce qui fait que le filtrage de texture virtuelle est souvent soumis à des limitations que les textures normales n'ont pas, notamment pour le filtrage anisotropique.
{{NavChapitre | book=Les cartes graphiques
| prev=Le rasterizeur
| prevText=Le rasterizeur
| next=Les Render Output Target
| nextText=Les Render Output Target
}}{{autocat}}
pbitcpguzqigttlcg9xy9nt4335mion
763417
763416
2026-04-10T18:48:22Z
Mewtow
31375
/* L'usage des textures usuelles pour les normal maps */
763417
wikitext
text/x-wiki
[[File:Texture mapping.png|vignette|''Texture mapping'']]
Les '''textures''' sont des images que l'on va plaquer sur la surface d'un objet, du papier peint en quelque sorte. Les cartes graphiques supportent divers formats de textures, qui indiquent comment les pixels de l'image sont stockés en mémoire : RGB, RGBA, niveaux de gris, etc. Une texture est donc composée de "pixels", comme toute image numérique. Pour bien faire la différence entre les pixels d'une texture, et les pixels de l'écran, les pixels d'une texture sont couramment appelés des ''texels''.
==Le placage de textures inverse==
Pour rappel, plaquer une texture sur un objet consiste à attribuer un texel à chaque sommet, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture.
Dans les faits, on n'utilise pas de coordonnées entières de ce type. Les coordonnées de texture sont deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. Le nom donnée à cette technique de description des coordonnées de texture s'appelle l''''''UV Mapping'''''.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Les API 3D modernes gèrent des textures en trois dimensions, ce qui ajoute une troisième coordonnée de texture notée w. Dans ce qui va suivre, nous allons passer les textures en trois dimensions sous silence. Elles ne sont pas très utilisées, la quasi-totalité des jeux vidéo et applications 3D utilisant des textures en deux dimensions. Par contre, le matériel doit gérer les textures 3D, ce qui le rend plus complexe que prévu. Il faut ajouter quelques circuits pour, de quoi gérer la troisième coordonnée de texture, etc.
Lors de la rastérisation, chaque fragment se voit attribuer un sommet, et donc la coordonnée de texture qui va avec. Si un pixel est situé pile sur un sommet, la coordonnée de texture de ce sommet est attribuée au pixel. Si ce n'est pas le cas, la coordonnée de texture finale est interpolée à partir des coordonnées des trois sommets du triangle rastérisé. L'interpolation en question a lieu dans l'étape de rastérisation, comme nous l'avons vu dans le chapitre précédent. Le fait qu'il y ait une interpolation fait que les coordonnées du pixel gagent à être des nombres flottants. On pourrait faire une interpolation avec des coordonnées de texture entières, mais les arrondis et autres imprécisions de calcul donneraient un résultat graphiquement pas terrible, et empêcheraient d'utiliser les techniques de filtrage de texture que nous verrons dans ce chapitre.
À partir de ces coordonnées de texture, la carte graphique calcule l'adresse du texel qui correspond, et se charge de le lire. Et toute la magie a lieu dans ce calcul d'adresse, qui part de coordonnées de texture flottante, pour arriver à une adresse mémoire. Le calcul de l'adresse du texel se fait en plusieurs étapes, que nous allons voir ci-dessous. La première étape convertit les coordonnées flottantes en coordonnées entières, qui disent à quel ligne et colonne se trouve le texel voulu dans la texture. L'étape suivante transforme ces coordonnées x,y entières en adresse mémoire.
===La normalisation des coordonnées===
J'ai dit plus haut que les coordonnées de texture sont des coordonnées flottantes, comprises entre 0 et 1. Mais il faut savoir que les pixels shaders peuvent modifier celles-ci pour mettre en œuvre certains effets graphiques. Et le résultat peut alors se retrouver en-dehors de l'intervalle 0,1. C'est quelque chose de voulu et qui est traité par la carte graphique automatiquement, sans que ce soit une erreur. Au contraire, la manière dont la carte graphique traite cette situation permet d'implémenter des effets graphiques comme des textures en damier ou en miroir.
[[File:Clamp tile.jpg|vignette|Clamp tile]]
Il existe globalement trois méthodes très simples pour gérer cette situation, qui sont appelés des '''modes d'adressage de texture'''.
* La première méthode est de faire en sorte que le résultat sature. Si une coordonnée est inférieur à 0, alors on la remplace par un zéro. Si elle est supérieure à 1, on la ramène à 1. Avec cette méthode, tout se passe comme si les bords de la texture étaient étendus et remplissaient tout l'espace autour de la texture. Le tout est illustré ci-dessous. Ce mode d'accès aux textures est appelé le '''''clamp'''''.
* Une autre solution retire la partie entière de la coordonnée, elle coupe tout ce qui dépasse 1. Pour le dire autrement, elle calcule le résultat modulo 1 de la coordonnée. Le résultat est que tout se passe comme si la texture était répétée à l'infini et qu'elle pavait le plan.
* Une autre méthode remplit les coordonnées qui sortent de l’intervalle 0,1 avec une couleur préétablie, configurée par le programmeur.
===La conversion des coordonnées de textures flottantes en adresse mémoire===
Une fois la normalisation effectuée, les coordonnées de texture sont utilisées pour lire le texel voulu. Pour cela, les coordonnées de texte sont transformées en adresse mémoire, adresse qui pointe sur le texel ayant ces cordonnées. Pour cela, la première étape est de transformer les coordonnées flottantes u,v en coordonnées entières x,y qui pointent sur un texel. Pour cela, il suffit de multiplier les coordonnées flottantes u,v par la résolution de la texture accédée. Pour un écran de résolution <math>\text{height,width}</math>, le calcul est le suivant :
: <math>x = u \times \text{width}</math>
: <math>y = v \times \text{height}</math>
Le résultat est un nombre avec une partie entière et une partie fractionnaire. La partie entière des deux coordonnées donne la position x,y voulue, et la partie fractionnaire est conservée pour le filtrage de textures, mais passons cela sous silence pour le moment.
La seconde étape prend les coordonnées entières x,y et calcule l'adresse mémoire du texel. L'adresse dépend de la position de la texture en mémoire, précisément de son début, son premier texel, mais aussi de la position du texel par rapport au début de la texture. Et calculer cette position intra-texture dépend de la manière dont les texels sont stockés en mémoire.
====Les textures naïves====
Les programmeurs qui lisent ce cours s'attendent certainement à ce que la texture soit stockée en mémoire ligne par ligne, ou colonne par colonne. Cela veut dire que le premier pixel en partant d'en haut à gauche est stocké en premier, puis celui immédiatement à sa droite, puis celui encore à droite, et ainsi de suite. Une fois qu'on arrive à la fin d'une ligne, on passe à la ligne suivante, en-dessous. Cette organisation ligne par ligne s'appele l'organisation '''''row major order'''''. On peut faire pareil, mais colonne par colonne, ce qui donne le '''''column major order'''''.
[[File:Speicheranordnung Feld.svg|centre|vignette|upright=2|Row et column major order.]]
Maintenant, supposons que la texture commence à l'adresse <math>A_\text{texture}</math>, qui est l'adresse du premier texel. La texture a une résolution de <math>\text{width}</math> texels de large et <math>\text{height}</math> texels de haut. Par définition, les coordonnées X et Y des texels commencent à 0, ce qui fait que le pixel en haut à gauche a les coordonnées 0,0.
L'adresse du pixel se calcule comme suit :
: <math>A_\text{pixel} = A_\text{texture} + (\text{taille d'une ligne en octets} \times Y) + (\text{taille d'un texel en octets} \times X)</math>
La taille d'un pixel en mémoire est notée T. La taille d'une ligne en mémoire est de <math>width \times T</math>, par définition, vu qu'elle fait <math>width</math> texels. On a donc :
: <math>A_\text{pixel} = A_\text{texture} + (width \times T \times Y) + (T \times X)</math>
La formule se réécrit comme suit :
: <math>A_\text{pixel} = A_\text{texture} + T \times (width \times Y + X)</math>
Le calcul d'adresse est donc assez simple. Malheureusement, les textures ne sont pas stockées de cette manière en mémoire vidéo. En effet, elle se marie mal avec les opérations de filtrage de texture que nous allons voir dans ce qui suit. Le filtrage d'un texel dépend de ses voisins du dessus et du dessous. Le fait que la texture n'est pas forcément parcourue ligne par ligne fait que stocker une texture ligne par ligne n'est pas l'idéal.
De même, les textures sont déformées par la perspective. L'affichage de la texture ne se fait alors pas ligne par ligne, mais en parcourant la texture en diagonale, l'angle de la diagonale correspondant approximativement à l'angle que fait la verticale de la texture avec le regard. Vu qu'on ne connait pas à l'avance l'angle que fera la diagonale de parcours, on doit ruser.
====Les textures tilées====
Une première solution à ce problème est celle des '''textures tilées'''. Avec ces textures, l'image de la texture est découpée en ''tiles'', des rectangles ou en carrés de taille fixe, généralement des carrés de 4 pixels de côté. Les tiles ont une largeur et une longueur égales, afin de simplifier les calculs : on divise X et Y par le même nombre. De plus, leur largeur et leur longueur sont une puissance de deux, afin de simplifier les calculs d'adresse. Les ''tiles'' sont alors mémorisée les unes après les autres dans le fichier de la texture.
[[File:Texture tilée.png|centre|vignette|upright=2|Texture tilée]]
La formule de calcul d'adresse vue plus haut doit être adaptée pour tenir compte des tiles. Pour cela, il faut remplacer la taille d'un texel par la taille d'une tile, et que la largeur de la texture soit exprimée en nombre de tiles. De plus, on doit adapter les coordonnées des texels pour donner des coordonnées de tile. Généralement, les tiles sont des carrés de N pixels de côté, ce qui fait qu'on peut regrouper les lignes et les colonnes par paquets de N. Il suffit donc de diviser Y et X pour obtenir les coordonnées de la tile, de même que la larguer. La formule pour calculer la position de la énième tile est alors la suivante :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + \text{Taille mémoire d'une tile} \times \left( {\text{Width} \over N} \times {Y \over N} + {X \over N} \right)</math>
On peut réécrire le tout comme suit :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + K \times \left( {Y \over N} + X \right)</math>, avec K une constante connue à la compilation des shaders.
Vu que les tiles sont carrées avec une largeur qui est une puissance de deux, la multiplication par la taille d'une tile en mémoire se simplifie : on passe d'une multiplication entière à des décalages de bits. Même chose pour le calcul de l'adresse de la tile à partir des coordonnées x,y : ils impliquent des divisions par une puissance de deux, qui deviennent de simples décalages.
La position d'un pixel dans une tile dépend du format de la texture, mais peut se calculer avec quelques calculs arithmétiques simples. Dans les cas les plus simples, les pixels sont mémorisés ligne par ligne, ou colonne par colonne. Mais ce n'est pas systématiquement le cas. Toujours est-il que les calculs pour déterminer l'adresse sont simples, et ne demandent que quelques additions ou multiplications. Mais avec les formats de texture utilisés actuellement, les tiles sont chargées en entier dans le cache de texture, sans compter que diverses techniques de compression viennent mettre le bazar, comme on le verra dans la suite de cours.
Un avantage de l'organisation en tiles est qu'elle se marie bien avec le parcours des textures. On peut parcourir une texture dans tous les sens, horizontal, vertical, ou diagonal, on sait que les prochains pixels ont de fortes chances d'être dans la même tile. Si on rentre dans une tile par la gauche en haut, on a encore quelques pixels à parcourir dans la tile, par exemple. De même, le filtrage de textures est facilité. On verra dans ce qui va suivre que le filtrage de texture a besoin de lire des blocs de 4 texels, des carrés de 2 pixels de côté. Avec l'organisation en tile, on est certain que les 4 texels seront dans la même tile, sauf s'ils ont le malheur d'être tout au bord d'une tile. Ce dernier cas est assez rare, et il l'est d'autant plus que les tiles sont grandes. Enfin, un dernier avantage est que les tiles sont généralement assez petites pour tenir tout entier dans une ligne de cache. Le cache de texture est donc utilisé à merveille, ce qui rend les accès aux textures plus rapides.
====Les textures basées sur des ''z-order curves''====
Les formats de textures théoriquement optimaux utilisent une '''''Z-order curve''''', illustrée ci-dessous. L'idée est de découper la texture en quatre rectangles identiques, et de stocker ceux-ci les uns à la suite des autres. L'intérieur de ces rectangles est lui aussi découpé en quatre rectangles, et ainsi de suite. Au final, l'ordre des pixels en mémoire est celui illustré ci-dessous.
[[File:Z-CURVE.svg|centre|vignette|upright=2|Construction d'une ''Z-order curve''.]]
Les texels sont stockés les uns à la suite des autres dans la mémoire, en suivant l'ordre donnée par la ''Z-order curve''. Le calcul d'adresse calcule la position du texel en mémoire, par rapport au début de la texture, et ajoute l'adresse du début de la texture. Mais tout le défi est de calculer la position d'un texel en mémoire, à partir des coordonnées x,y. Le calcul peut sembler très compliqué, mais il n'en est rien. Le calcul demande juste de regarder les bits des deux coordonnées et de les combiner d'une manière particulièrement simple. Il suffit de placer le bit de poids fort de la coordonnée x, suivi de celui de la coordonnée y, et de faire ainsi de suite en passant aux bits suivants.
[[File:Zcurve45bits.png|centre|vignette|upright=1.5|Calcul de la position d'un élément dans une ''Z-order curve'' à partir des coordonnées x et y.]]
L'avantage d'une telle organisation est que la textures est découpées en ''tiles'' rectangulaires d'une certaine taille, elles-mêmes découpées en ''tiles'' plus petites, etc. Et il se trouve que cette organisation est parfaite pour le cache de texture. L'idéal pour le cache de texture est de charger une ''tile'' complète dans le cache de textures. Quand on accède à un texel, on s'assure que la ''tile'' complète soit chargée. Mais cela demande de connaitre à l'avance la taille d'une ''tile''. Les formats de texture fournissent généralement une ''tile'' carré de 4 pixels de côté, mais cela donnerait un cache trop petit pour être vraiment utile. Avec cette méthode, on s'assure qu'il y ait une ''tile'' avec la taille optimale. Les ''tiles'' étant découpées en ''tiles'' plus petites, elles-mêmes découpées, et ainsi de suite, on s'assure que la texture est découpées en ''tiles'' de taille variées. Il y aura au moins une ''tile'' qui rentrera tout pile dans le cache.
==Les techniques de rendu à textures multiples==
Nous venons de voir comment une texture est plaquée sur un objet 3D, ou une surface comme un sol. Pour résumer, le calcul de l'adresse d'un texel prend la position du texel par rapport au début de la texture, et ajoute l'adresse du début de la texture. L'adresse mémoire de la texture est connue au moment où le pilote de la carte graphique place la texture dans la mémoire vidéo, et cette information est transmise au matériel par l'intermédiaire du processeur de commande, puis passée aux processeurs de shaders et à l'unité de texture. Le tout est couplé à d'autres informations, la plus importante étant la ''taille de la texture en octets'', pour éviter de déborder lors des accès à la texture.
Néanmoins, il s'agit là du cas le plus simple. Certaines techniques de rendu demandent de choisir la texture à plaquer parmi un ensemble de plusieurs textures. Les techniques en question sont assez variées et n'ont pas grand chose en commun. Les plus connues sont le ''mip-mapping'', le ''cube-mapping'' et les textures virtuelles. Le ''mip-mapping'' sert à filtrer les textures, chose qu'on expliquera plus tard, le ''cube-mapping'' sert à simuler des réflexions sur un objet en plaquant une texture de l'environnement dessus, les textures virtuelles sont une optimisation pour les textures des terrains de grande taille. Mais malgré leurs différences, elles demandent de choisir quelle texture plaquer entre plusieurs textures de base. En clair, l'adresse de base de la texture varie selon la situation. Voyons-les dans le détail.
===Le mip-mapping===
Le '''mip-mapping''' a pour but de légèrement améliorer les graphismes des objets lointains, tout en rendant les calculs de texture plus rapides. Formellement, le ''mip-mapping'' est une technique de filtrage de texture, mais nous l'abordons maintenant car elle est surtout liée au calcul d'adresse. Les unités de texture ont des circuits de filtrage de texture séparés des circuits de ''mip-mapping'' et de calcul d'adresse, d'où le fait que nous en parlons séparément.
Le problème résolu par le ''mip-mapping'' est le rendu des textures lointaines. Si une texture est plaquée sur un objet lointain, une bonne partie des détails est invisible pour l'utilisateur. Un pixel de l'écran est associé à plusieurs texels. Idéalement, la carte graphiques devrait lire tous ces texels et en faire une sorte de moyenne pondérée, pour calculer la couleur finale du pixel. Mais dans les faits, ce serait très gourmand et compliqué à implémenter en hardware. Une solution serait de ne garder que quelque texels, mais cela a tendance à créer des artefacts visuels (les textures affichées ont tendance à pixeliser). Le ''mip-mapping'' permet de réduire ces deux problèmes en même temps en précalculant cette moyenne pondérée pour des distances prédéfinies.
L'idée est d'utiliser plusieurs exemplaires d'une même texture à des résolutions différentes, chaque exemplaire étant adapté à une certaine distance. Par exemple, une texture sera stocké avec un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre de 128 * 128 et ainsi de suite jusqu’à un dernier exemplaire de 32 * 32 pixel. Chaque exemplaire correspond à un '''niveau de détail''', aussi appelé ''Level Of Detail'' (abrévié en LOD). La résolution utilisée diminue d'autant plus que l'objet est situé loin de la caméra. Les objets proches seront rendus avec la texture 512*512, ceux plus lointains seront rendus avec la texture de résolution 256*256, les textures 128*128 seront utilisées encore plus loin, et ainsi de suite jusqu'aux objets les plus lointains qui sont rendus avec la texture la plus petite de 32*32.
[[File:MipMap Example STS101.jpg|centre|vignette|upright=2|Exemples de mip-maps.]]
Le ''mip-mapping'' améliore grandement la qualité d'image. L'image d'exemple ci-dessous le montre assez bien.
[[File:Mipmapping example.png|centre|vignette|upright=2|Exemple de mipmapping.]]
Pour faciliter les calculs d'adresse, les LOD d'une même texture sont stockées les uns après les autres en mémoire (dans un tableau, comme diraient les programmeurs). Ainsi, pas besoin de se souvenir de la position en mémoire de chaque LOD : l'adresse de la texture de base, et quelques astuces arithmétiques suffisent. Prenons le cas où la texture de base a une taille L. le premier exemplaire est à l'adresse 0, le second niveau de détail est à l'adresse L, le troisième à l'adresse L + L/4, le suivant à l'adresse L + L/4 + L/16, et ainsi de suite. Le calcul d'adresse demande juste connaître le niveau de détails souhaité et l'adresse de base de la texture. Le niveau de détail voulu est calculé par les pixel shaders, en fonction de la coordonnée de profondeur du pixel à traiter.
Évidemment, cette technique consomme de la mémoire vidéo, vu que chaque texture est dupliquée en plusieurs exemplaires, en plusieurs LOD. Dans le détail, la technique du mip-mapping prend au maximum 33% de mémoire en plus (sans compression). Cela vient du fait qu'en prenant une texture dexu fois plus petite, elle prend 4 fois moins de mémoire : 2 fois moins de pixels en largeur, et 2 fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des LODs, texture de base comprise, prendra X + (X/4) + (X/16) + (X/256) + … Un petit calcul de limite donne 4/3 * X, soit 33% de plus.
===Le cube-mapping===
[[File:Cube mapped reflection example 2.JPG|vignette|Exemple de reflets environnementaux.]]
L''''environnement-mapping''' est une technique de calcul de divers effets graphiques liés à l'environnement, notamment des réflexions. L'idée est de plaquer une texture pré-calculée pour simuler l'effet de l'environnement sur une surface ou un objet 3D. Il en existe plusieurs versions différentes, mais la seule utilisée de nos jours est le ''cube-mapping'', où la texture de l'environnement est plaquée sur un cube, d'où son nom. Le cube en question est utilisé différemment suivant ce que l'on cherche à faire avec le ''cube-mapping''. Les deux utilisations principales sont le rendu du ciel et des décors, et les réflexions sur la surface des objets. Dans les deux cas, l'idée est de précalculer ce que l'on voit du point de vue de la caméra. On place la caméra dans la scène 3D, on place un cube centré sur la caméra, le cube est texturé avec ce que l'on voit de l'environnement depuis la caméra/l'objet de son point de vue.
[[File:Panorama cube map.png|centre|vignette|upright=2|L'illustration montre en premier lieu une ''cubemap'' avec les six faces mises en évidence, puis quel environnement 3D elle permet de simuler, le troisième illustration montrant comment la ''cubemap'' est utilisée pour simuler l'environnement.]]
Le rendu du ciel et des décors lointains dans les jeux vidéo se base sur des '''''skybox''''', à savoir un cube centré sur la caméra, sur lequel on ajoute des textures de ciel ou de décors lointains. Le cube est recouvert par une texture, qui correspond à ce que l'on voit quand on dirige le regard de la caméra vers cette face. Contrairement à ce qu'on pourrait croire, la skybox n'est pas les limites de la scène 3D, les limites du niveau d'un jeu vidéo ou quoique ce soit d'autre de lié à la physique de la scène 3D. La skybox est centrée sur la caméra, elle suit la caméra dans son mouvement. Centrer la skybox sur la caméra permet de simuler des décors très lointains, suffisamment lointain pour qu'on n'ait pas l'illusion de s'en rapprocher en se déplaçant dans la map. De plus, cela évite d'avoir à faire trop de calculs à chaque fois que l'on bouge la caméra. La texture plaquée sur le cube est une texture unique, elle-même découpée en six sous-textures, une par face du cube.
[[File:Skybox example.png|centre|vignette|upright=2|Exemple de Skybox.]]
[[File:Cube mapped reflection example.jpg|vignette|Réflexions calculées par une ''cubemap''.]]
Le ''cube-mapping'' est aussi utilisé pour des reflets. L'idée est de simuler les reflets en plaquant une texture pré-calculée sur l'objet réflecteur. La texture pré-calculée est un dessin de l'environnement qui se reflète sur l'objet, un dessin du reflet à afficher. En la plaquant la texture sur l'objet, on simule ainsi des reflets de l'environnement, mais on ne peut pas calculer d'autres reflets comme les reflets objets mobiles comme les personnages. Et il se trouve que la texture pré-calculée est une ''cubemap''. Pour les environnements ouverts, c'est la ''skybox'' qui est utilisée, ce qui permet de simuler les reflets dans les flaques d'eau ou dans des lacs/océans/autres. Pour les environnements intérieurs, c'est une cubemap spécifique qui utilisée. Par exemple, pour l'intérieur d'une maison, on a une ''cubemap'' par pièce de la maison. Les reflets se calculent en précisant quelle ''cubemap'' appliquer sur l'objet en fonction de la direction du regard.
[[File:Cube map level.png|centre|vignette|Cube map de l'intérieur d'une pièce d'un niveau de jeux vidéo.]]
Toujours est-il que les textures utilisées pour le ''cubemmapping'', appelées des ''cubemaps'', sont en réalité la concaténation de six textures différentes. En mémoire vidéo, la ''cubemap'' est stockée comme six textures les unes à la suite des autres. Lors du rendu, on doit préciser quelle face du cube utiliser, ce qui fait 6 possibilités. On a le même problème qu'avec les niveaux de détail, sauf que ce sont les faces d'une ''cubemap'' qui remplacent les textures de niveaux de détails. L'accès en mémoire doit donc préciser quelle portion de la ''cubemap'' il faut accéder. Et l'accès mémoire se complexifie donc. Surtout que l'accès en question varie beaucoup suivant l'API graphique utilisée, et donc suivant la carte graphique.
Les API 3D assez anciennes ne gérent pas nativement les ''cubemaps'', qui doivent être émulées en logiciel en utilisant six textures différentes. Le pixel shader décide donc quelle ''cubemap'' utiliser, avec quelques calculs sur la direction du regard. L'accès se fait d'une manière assez simple : le shader choisit quelle texture utiliser. Les API 3D récentes gèrent nativement les ''cubemaps''. Dans le cas le plus simple,pour les versions les plus vielles de ces API, les six faces sont numérotées et l'accès à une ''cubemap'' précise quel face utiliser en donnant son numéro. La carte graphique choisit alors automatiquement la bonne texture, mais cela demande de laisser le calcul de la bonne face au pixel shader. D'autres API 3D et cartes graphiques font autrement. Dans les API 3D modenres, les ''cubemap'' sont gérées comme des textures en trois dimensions, adressées avec trois coordonnées u,v,w. La carte graphique utilise ces trois coordonnées de manière à en déduire quelle est la face pertinente, mais aussi les coordonnées u,v dans la texture de la face.
==L'implémentation matérielle du placage de textures==
Pour résumer, la lecture d'un texel demande d'effectuer plusieurs étapes. Dans le cas le plus simple, sans ''mip-mapping'' ou ''cubemapping'', on doit effectuer les étapes suivantes :
* Il faut d'abord normaliser les coordonnées de texture pour qu'elles tombent dans l'intervalle [0,1] en fonction du mode d'adressage désiré.
* Ensuite, les coordonnées u,v doivent être converties en coordonnées entières, ce qui demande une multiplication flottante.
* Enfin, l'adresse finale est calculée à partir des coordonnées entières et en ajoutant l'adresse de base de la texture (et éventuellement avec d'autres calculs arithmétiques suivant le format de la texture).
Tout cela pourrait être fait par le pixel shaders, mais cela implique beaucoup de calculs répétitifs et d'opérations arithmétiques assez lourdes, avec des multiplications flottantes, des additions et des multiplications entières, etc. Faire faire tous ces calculs par les shaders serait couteux en performance, sans compter que les shaders deviendraient plus gros et que cela aurait des conséquences sur le cache d'instruction. De plus, certaines de ces étapes peuvent se faire en parallèle, comme les deux premières, ce qui colle mal avec l'aspect sériel des shaders.
Aussi, les processeurs de shaders incorporent une unité de calcul d'adresse spéciale pour faire ces calculs directement en matériel. L'unité de texture contient au minimum deux circuits : un circuit de calcul d'adresse, et un circuit d'accès à la mémoire. Toute la difficulté tient dans le calcul d'adresse, plus que dans le circuit de lecture. Le calcul d'adresse est conceptuellement réalisé en deux étapes. La première étape qui transforme les coordonnées u,v en coordonnées x,y qui donne le numéro de la ligne et de la colonne du texel dans la texture. La seconde étape prend ces deux coordonnées x,y, l'adresse de la texture, et détermine l'adresse de la tile à lire.
[[File:Unité de texture simple.png|centre|vignette|upright=2|Unité de texture simple]]
===L'implémentation du mip-mapping===
Le ''mip-mapping'' est lui aussi pris en charge par l'unité de calcul d'adresse, car cette technique change l'adresse de base de la texture. La gestion du ''mip-mapping'' est cependant assez complexe. Il est possible de laisser le pixel shader calculer quel niveau de détail utiliser, en fonction de la coordonnée de profondeur z du pixel à afficher. La carte graphique détermine alors automatiquement quelle texture lire, quel niveau de détail, automatiquement. Elle détermine aussi la bonne résolution pour la texture, qui est égal à la résolution de la texture de base, divisée par le niveau de détail. Pour résumer, le niveau de détail est envoyé aux unités de texture, qui s'occupent de calculer l'adresse de base et la résolution adéquates. Quelques calculs arithmétiques simples, donc, qui s'implémentent facilement avec quelques circuits.
Mais une autre méthode laisse la carte graphique déterminer le niveau de détail par elle-même. Dans ce cas, cela demande, outre les deux coordonnées de texture, de calculer la dérivée de ces deux coordonnées dans le sens horizontal et vertical, ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Les quatre dérivées sont les suivantes :
: <math>\frac{du}{dx}</math>, <math>\frac{dv}{dx}</math>, <math>\frac{du}{dy}</math>, <math>\frac{dv}{dy}</math>
Un bon moyen pour obtenir les dérivées demande de regrouper les pixels par groupes de 4 et de faire la différence entre leurs coordonnées de texture respectives. On peut calculer les deux dérivées horizontales en comparant les deux pixels sur la même ligne, et les deux dérivées verticales en comparant les deux pixels sur la même colonne. Mais cela demande de rastériser les pixels par groupes de 4, par ''quads''. Et c'est ce qui est fait sur les cartes graphiques actuelles, qui rastérisent des groupes de 4 pixels à la fois.
[[File:Texture sampler unit with mipmapping.png|centre|vignette|upright=2.0|Unité de texture avec mipmapping.]]
Malheureusement, le calcul exact utilisé pour le choix de la mip-map dépend du GPU considéré et peu de chose est connu quant à ces algorithmes. Il est possible d'inférer le comportement à partir d'observations, mais guère plus. Pour ceux qui veulent en savoir plus, je conseille la lecture de cet article de blog :
* [https://pema.dev/2025/05/09/mipmaps-too-much-detail/ Mipmap selection in too much detail]
===La gestion des accès mémoire===
Enfin, l'unité de texture doit tenir compte du fait que la mémoire vidéo met du temps à lire une texture. En théorie, l'unité de texture ne devrait pas accepter de nouvelle demande de lecture tant que celle en cours n'est pas terminée. Mais faire ainsi demanderait de bloquer tout le pipeline, de l'''input assembler'' au unités de''shaders'', ce qui est tout sauf pratique et nuirait grandement aux performances.
Une solution alternative consiste à mettre en attente les demandes de lectures de texture pendant que la mémoire est occupée. La manière la plus simple d'implémenter des accès mémoire multiples est de les mettre en attente dans une petite mémoire FIFO. Cela implique que les accès mémoire s’exécutent dans l'ordre demandé par le ''shader'' et/ou l'unité de rastérisation, il n'y a pas de réorganisation des accès mémoire ou d’exécution dans le désordre des accès mémoire.
[[File:Texture prefetching.png|centre|vignette|upright=1.5|Accès mémoire simultanés.]]
Évidemment, quand la mémoire FIFO est pleine, le pipeline est alors totalement bloqué. Le rasteriser est prévenu que l'unité de texture ne peut pas accepter de nouvelle lecture de texture. En pratique, la FIFO est généralement d'une taille respectable et permet de mettre en attente beaucoup de demandes de lecture de texture. Il faut de plus noter qu'il y a une FIFO par processeur de ''shader'' sur les cartes graphiques modernes. Quand elle est pleine, le processeur cesse d'exécuter de nouveaux accès mémoire, mais peut continuer à exécuter des ''shaders'' dans les autres unités de calcul, pas besoin de bloquer complétement le pipeline.
===L'intégration du cache de textures===
Il faut noter que les unités de texture incorporent aussi un cache de texture, voire plusieurs. L'intégration des caches de texture avec la mémoire FIFO précédente est quelque peu compliqué, car il faut garantir que les lectures de texture se fassent dans le bon ordre. On ne peut pas exécuter une lecture dans le cache alors que des lectures précédentes sont en attente de lecture en mémoire vidéo. Et cela pose un gros problème : une lecture dans le cache de texture prend quelques dizaines de cycles d'horloge, alors qu'une lecture en mémoire vidéo en prend facilement 400 à 800 cycles, parfois plus. Et cela fait que l'ordre des accès mémoire peut s'inverser.
Prenons par exemple un accès au cache précédé et suivi par deux accès en mémoire vidéo. Le premier démarre au cycle 1, et se termine au cycle numéro 400. L'accès au cache commence au cycle 2 et se termine 20 cycles après, au cycle numéro 22. En clair, la lecture dans le cache s'est terminée avant l'accès mémoire qui le précède. Les textures ne sont donc plus lues dans l'ordre. Et il faut trouver une solution pour éviter cela.
La solution est de retarder les lectures dans le cache tant que tous les accès précédents ne sont pas terminés. Mais pour retarder les lectures en question, il faut d'abord savoir si la lecture atterrit dans le cache ou non, ce qui demande d'accéder au cache. On fait face à un dilemme : on veut retarder les accès au cache, mais les différencier des lectures déclenchant des accès mémoire demande d'accéder au cache en premier lieu. La solution est décrite dans l'article "Prefetching in a Texture Cache Architecture" par Igehy et ses collègues. Elle se base sur deux idées combinées ensemble.
La première idée est de séparer l'accès au cache en deux : une étape qui vérifie si les texels à lire sont dans le cache, et une étape qui accède aux données dans le cache lui-même. Un cache de texture est donc composé de deux circuits principaux. Le premier vérifie la présence des texels dans le cache. Il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Ensuite, en plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
La seconde idée est de retarder l'accès au cache entre les deux phases. La première étape d'un accès mémoire vérifie si la donnée est dans le cache ou non. Puis, on retarde la lecture des données, pour attendre que toutes les lectures précédentes soient terminées. Et enfin, troisième étape : la lecture des texels dans la mémoire cache proprement dite. Les accès mémoire passant par la mémoire vidéo se font de la même manière, à une différence près : la lecture dans le cache est remplacée par la lecture en mémoire vidéo. Tout démarre avec une demande à l'unité de tags, qui vérifie si le texel est dans le cache ou non. Puis on retarde l'accès tant que la mémoire vidéo est occupée, puis on effectue la lecture en mémoire vidéo.
Si ce n'est pas le cas, l'accès mémoire est envoyé à la mémoire vidéo comme précédemment, à savoir qu'il est mis en attente dans une mémoire FIFO, puis envoyé à la mémoire vidéo dès que celle-ci est libre. Mais en sortie de la mémoire, la donnée lue est envoyée dans le cache de texture, par dans l'unité de filtrage. Pour savoir où placer la donnée lue, l'unité de tag a réservé une ligne de cache précise, une adresse bien précise. L'adresse en question est disponible en lisant une autre mémoire FIFO, qui a mis en attente l'adresse en question, en attendant que l'accès mémoire se termine. La donnée est alors écrite dans le cache, puis lue par l'unité de filtrage de textures.
Pour une lecture dans le cache, le déroulement est similaire, mais sans le passage par la mémoire. La lecture fait une demande à l'unité de tag, et celle-ci répond que la donnée est bien dans le cache. Elle place alors l'adresse à lire dans la file d'attente. Une fois que les accès mémoire précédents sont terminés, l'adresse sort de la file d'attente et est envoyée à la mémoire de données. La lecture s'effectue, les texels sont envoyés à l'unité de filtrage de textures. La seule différence avec un ''phased cache'' normal est l'insertion de l'adresse à lire dans une FIFO qui vise à mettre en attente
[[File:Unité de texture avec un cache de texture.png|centre|vignette|upright=2.0|Unité de texture avec un cache de texture]]
Pour résumer, l'implémentation précédente garantit une exécution des lectures dans leur ordre d'arrivée. Et pour cela, elle retarde les lectures dans le cache tant que les lectures en mémoire précédentes ne sont pas terminées. L'accès au cache est plus rapide que l'accès en mémoire vidéo, mais le retard ajouté pour garantir l'ordre des lectures fait que le temps d'accès est très long.
==Le filtrage de textures==
Plaquer des textures sans autre forme de procès ne suffit pas à garantir des graphismes d'une qualité époustouflante. La raison est que les sommets et les texels ne tombent pas tout pile sur un pixel de l'écran : le sommet associé au texel peut être un petit peu trop en haut, ou trop à gauche, etc. Une explication plus concrète fait intervenir les coordonnées de texture. Souvenez-vous que lorsque l'on traduit une coordonnée de texture u,v en coordonnées x,y, on obtient un résultat qui ne tombe pas forcément juste. Souvent, le résultat a une partie fractionnaire. Si celle-ci est non-nulle, cela signifie que le texel/sommet n'est pas situé exactement sur le pixel voulu et que celui-ci est situé à une certaine distance. Concrètement, le pixel tombe entre quatre texels, comme indiqué ci-dessous.
[[File:Filtrage texture.png|centre|vignette|upright=2.0|Position du pixel par rapport aux texels.]]
Pour résoudre ce problème, on doit utiliser différentes techniques d'interpolation, aussi appelées techniques de '''filtrage de texture''', qui visent à calculer la couleur du pixel final en fonction des texels qui l'entourent. Il existe de nombreux types de filtrage de textures, qu'il s'agisse du filtrage linéaire, bilinéaire, trilinéaire, anisotropique et bien d'autres.
Tous ont besoin d'avoir certaines informations qui sont généralement fournies par les circuits de calcul d'adresse. La première est clairement la partie fractionnaire des coordonnées x,y. La seconde est la dérivée de ces deux coordonnées dans le sens horizontal et vertical., ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Toujours est-il que le filtrage de texture est une opération assez lourde, qui demande beaucoup de calculs arithmétiques. On pourrait en théorie le faire dans les pixels shaders, mais le cout en performance serait absolument insoutenable. Aussi, les cartes graphiques intègrent toutes un circuit dédié au filtrage de texture, le ''texture sampler''. Même les plus anciennes cartes graphiques incorporent une unité de filtrage de texture, ce qui nous montre à quel point cette opération est importante.
[[File:Texture unit.png|centre|vignette|upright=2.0|Unité de texture.]]
On peut configurer la carte graphique de manière à ce qu'elle fasse soit du filtrage bilinéaire, soit du filtrage trilinéaire, on peut configurer le niveau de filtrage anisotropique, etc. Cela peut se faire dans les options de la carte graphique, mais cela peut aussi être géré par l'application. La majorité des jeux vidéos permettent de régler cela dans les options. Ces réglages ne concernent pas la texture elle-même, mais plutôt la manière dont l'unité de texture doit fonctionner. Ces réglages sur l''''état de l'unité de texture''' sont mémorisés quelque part, soit dans l'unité de texture elle-même, soit fournies avec la ressource de texture elle-même, tout dépend de la carte graphique. Certaines cartes graphiques mémorisent ces réglages dans les unités de texture ou dans le processeur de commande, et tout changement demande alors de réinitialiser l'état des unités de texture, ce qui prend un peu de temps. D'autres placent ces réglages dans les ressources de texture elles-mêmes, ce qui rend les modifications de configuration plus rapides, mais demande plus de circuits. D'autres cartes graphiques mélangent les deux options, certains réglages étant globaux, d'autres transmis avec la texture. Bref, difficile de faire des généralités, tout dépend du matériel et le pilote de la carte graphique cache tout cela sous le tapis.
Maintenant que cela est dit, voyons quelles sont les différentes méthodes de filtrage de texture et comment la carte graphique fait pour les calculer.
===Le filtrage au plus proche===
La méthode de filtrage la plus simple consiste à colorier avec le texel le plus proche. Cela revient tout simplement à ne pas tenir compte de la partie fractionnaire des coordonnées x,y, ce qui est très simple à implémenter en matériel. C'est ce que l'on appelle le '''filtrage au plus proche''', aussi appelé ''nearest filtering''.
Autant être franc, le résultat est assez pixelisé et peu agréable à l’œil. Par contre, le résultat est très rapide à calculer, vu qu'il ne demande aucun calcul à proprement parler. Elle ne fait pas appel à la parti fractionnaire des coordonnées entières de texture, ni aux dérivées de ces coordonnées. On peut combiner cette technique avec le mip-mapping, ce qui donne un résultat bien meilleur, bien que loin d'être satisfaisant. Au passage, toutes les techniques de filtrage de texture peuvent se combiner avec du mip-mapping, certaines ne pouvant pas faire sans.
[[File:Interpolation-nearest.svg|centre|vignette|Filtrage de texture au plus proche.]]
===Le filtrage linéaire===
Le filtrage le plus simple est le '''filtrage linéaire'''. Il effectue une interpolation linéaire entre deux mip-maps, deux niveaux de détails. Pour comprendre l'idée, nous allons prendre une situation très simple, avec une texture carrée de 512 texels de côté. Le mip-mapping crée plusieurs textures : une de 256 texels de côté, une de 128 texels, une de 64, etc. Maintenant, la texture est sur un objet à une certaine distance de l'écran, vu de face. Le résultat est qu'elle correspond à l'écran à un carré de 300 pixels de côté (pas d'erreur : pixels, pas texels). Dans ce cas, la texture se trouve entre deux mip-maps : celle de 512 pixels de côté, celle de 256. Laquelle choisir ? Le filtrage au plus proche prend la texture de 512 pixels de côté. Le filtrage linéaire lui, fait autrement.
Vu que la texture est entre deux mip-maps, l'idée est de prendre le texel au plus proche dans chaque texture et de faire une sorte de moyenne appelée l'interpolation linéaire. L'interpolation par du principe que la couleur varie entre les deux texels en suivant une fonction affine, illustrée ci-dessous. Ce ne serait évidemment pas le cas dans le monde réel, mais on supposer cela donne une bonne approximation de ce à quoi ressemblerait une texture à plus haute résolution. On peut alors calculer la couleur du pixel par une simple moyenne pondérée par la distance. Le résultat est que les transitions entre deux niveaux de détails sont plus lisses, moins abruptes.
[[File:Lin interp -é.png|centre|vignette|upright=2.0|Interpolation linéaire.]]
===Le filtrage bilinéaire===
Le filtrage bilinéaire effectue une sorte de moyenne pondérée des quatre texels les plus proches du pixel à afficher. Pour cela, rappelez-vous ce qui a été dit plus haut : les coordonnées x,y d'un pixel ont une partie entière et une partie fractionnaire. Le filtrage au plus proche élimine les parties fractionnaires, ce qui donne une coordonnée x,y. Avec le filtrage bilinéaire, on prend les texels de coordonnées (x,y) ; (x+1,y) ; (x,y+1) ; (x+1,y+1), le pixel étant entre ces 4 texels.
Mais le filtrage ne fait pas qu'une simple moyenne, il prend en compte les parties fractionnaires pour faire la moyenne. En effet, le pixel n'est pas au milieu du carré de texel, il est quelque part mais est souvent plus proche d'un texel que des autres. Et il faut donc pondérer la moyenne par les distances aux 4 texels. Pour cela, la moyenne est calculée à partir d'interpolations linéaires. Avec 4 pixels, nous allons devoir calculer la couleur de deux points intermédiaires. La couleur de ces deux points se calcule par interpolation linéaire, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.
[[File:Bilin3.png|centre|vignette|upright=2|Filtrage bilinéaire de texture.]]
Le circuit qui permet de faire l'interpolation bilinéaire est particulièrement simple. On trouve un circuit de chaque pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu, et un pour la transparence. Chacun de ces circuit est composé de sous-circuits chargés d'effectuer une interpolation linéaire, reliés comme suit.
[[File:Texture sampler unit.png|centre|vignette|Unité de filtrage bilinéaire.]]
Vous noterez que le filtrage bilinéaire accède à 4 pixels en même temps. Fort heureusement, les textures sont stockées de manière à ce qu'on puisse charger les 4 pixels en une fois, comme on l'a vu plus haut. Le filtrage bilinéaire a de fortes chances que les 4 pixels filtrés soient dans la même ''tile'', la seule exception étant quand ils sont tout juste sur le bord d'une ''tile''.
: La console de jeu Nintendo 64 n'utilise que trois pixels au lieu de quatre dans son interpolation bilinéaire, qui en devient une interpolation quasi-bilinéaire. La raison derrière ce choix est une question de performances, comme beaucoup de décisions de ce genre. Le résultat est un rendu imparfait de certaines textures.
===Le filtrage trilinéaire===
Avec le filtrage bilinéaire, des discontinuités apparaissent sur certaines surfaces. Par exemple, pensez à une texture de sol : elle est appliquée plusieurs fois sur toute la surface du sol. A une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512*512 à 256*256, ce qui est visible pour un joueur attentif. De telles transitions sont lissées grâce au filtrage linéaire, il n'y a plus qu'à le combiner avec le filtrage bilinéaire. Rien d’incompatible : le premier filtre l'intérieur d'une mip-map, le second combine deux mip-maps.
Le filtrage trilinéaire prend les deux mip-maps les plus proches, fait un filtrage bilinéaire avec chacune, puis fait une « une moyenne » pondérée entre les deux résultats. Le circuit de filtrage trilinéaire existe en plusieurs versions. La plus simple, illustrée ci-dessous, effectue deux filtrages bilinéaires en parallèle, dans deux circuits séparés, puis combine leurs résultats avec un circuit d'interpolation linéaire. Mais ce circuit nécessite de charger 8 texels simultanément. Qui plus est, ces 8 texels ne sont pas consécutifs en mémoire, car ils sont dans deux niveaux de détails/mip-maps différents.
[[File:Parallel trilinear filtering.png|centre|vignette|upright=2.0|Unité de filtrage trilinéaire parallèle.]]
Vu qu'on lit des texels dans deux mip-maps, les texels sont lus en deux fois : 4 texels provenant de la première mip-map, suivis par les 4 texels de l'autre mip-map. Les 4 premiers texels doivent donc être mis en attente dans des registres, en attendant que les 4 autres arrivent. Une amélioration du circuit précédent gère cela en ajoutant des registres. Il lit les 4 premiers texels, les filtre avec une interpolation bilinéaire, et mémorise le résultat dans un registre. Puis, il lit les 4 autres texels, les filtre, et met le résultat dans un second registre. A ce moment là, un circuit d'interpolation linéaire finit le travail. On économise donc un circuit d'interpolation bilinéaire, sans que les performances soient trop impactées.
[[File:Filtrage trilineaire.png|centre|vignette|upright=1.0|Unité de filtrage trilineaire série.]]
Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le temps où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. À chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients, et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.
===Le filtrage anisotrope===
D'autres artefacts peuvent survenir lors de l'application d'une texture, la perspective pouvant déformer les textures et entraîner l'apparition de flou. La raison à cela est que les techniques de filtrage de texture précédentes partent du principe que la texture est vue de face. Prenez une texture carrée, par exemple. Vue de face, elle ressemble à un carré sur l'écran. Mais tournez la caméra, de manière à voir la texture de biais, avec un angle, et vous verrez que la forme de la texture sur l'écran est un trapèze, pas un carré. Cette déformation liée à la perspective n'est pas prise en compte par les méthodes de filtrage de texture précédentes. Pour le dire autrement, les techniques de filtrage précédentes partent du principe que les 4 texels qui entourent un pixel forment un carré, ce qui est vrai si la texture est vue de face, sans angle, mais ne l'est pas si la texture n'est pas perpendiculaire à l'axe de la caméra. Du point de vue de la caméra, les 4 texels forment un trapèze d'autant moins proche d'un carré que l'angle est grand.
Pour corriger cela, les chercheurs ont inventé le '''filtrage anisotrope'''. En fait, je devrais plutôt dire : LES filtrages anisotropes. Il en existe un grand nombre, dont certains ne sont pas utilisés dans les cartes graphiques actuelles, soit car ils trop gourmand en accès mémoires et en calculs pour être efficaces, soit car ils ne sont pas pratiques à mettre en œuvre. Il est très difficile de savoir quelles sont les techniques de filtrage de texture utilisées par les cartes graphiques, qu'elles soient récentes ou anciennes. Beaucoup de ces technologies sont brevetées ou gardées secrètes, et il faudrait vraiment creuser les brevets déposés par les fabricants de GPU pour en savoir plus. Les algorithmes en question seraient de plus difficiles à comprendre, les méthodes mathématiques cachées derrière ces méthodes de filtrage n'étant pas des plus simple.
[[File:Anisotropic filtering en.png|centre|vignette|upright=2|Exemple de filtrage anisotrope.]]
==La compression de textures==
Les textures les plus grosses peuvent aller jusqu'au mébioctet, ce qui est beaucoup. Pour limiter la casse, les textures sont compressées. La '''compression de texture''' réduit la taille des textures, ce qui peut se faire avec ou sans perte de qualité. Elle entraîne souvent une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande. Mais il s'agit là d'une technique très simple, beaucoup plus simple que les techniques que nous allons voir dans cette section. Nous allons voir quelque algorithmes de compression de textures de complexité intermédiaire, mais n'allons pas voir l'état de l'art. Il existe des formats de texture plus récents que ceux qui nous allons aborder, comme l{{'}}''Ericsson Texture Compression'' ou l{{'}}''Adaptive Scalable Texture Compression'', plus complexes et plus efficaces.
Notons que les textures sont compressées dans les fichiers du jeu, mais aussi en mémoire vidéo. Les textures sont décompressées lors de la lecture. Pour cela, la carte graphique contient alors un circuit, capable de décompresser les textures lorsqu'on les lit en mémoire vidéo. Les cartes graphiques supportent un grand nombre de formats de textures, au niveau du circuit de décompression. Du fait que les textures sont décompressées à la volée, les techniques de compression utilisées sont assez particulières. La carte graphique ne peut pas décompresser une texture entière avant de pouvoir l'utiliser dans un ''pixel shader''. A la place, on doit pouvoir lire un morceau de texture, et le décompresser à la volée. On ne peut utiliser les méthodes de compression du JPEG, ou d'autres formats de compression d'image. Ces dernières ne permettent pas de décompresser une image morceau par morceau.
Pour permettre une décompression/compression à la volée, les textures sont des textures tilées, généralement découpées en tiles de 4 * 4 texels. Les ''tiles'' sont compressées indépendamment les unes des autres. Et surtout, avec ou sans compression, la position des tiles en mémoire ne change pas. On trouve toujours une tile tous les T octets, peu importe que la tile soit compressée ou non. Par contre, une tile compressée n'occupera pas T octets, mais moins, là où une tile compressée occupera la totalité des T octets. En clair, compresser une tile fait qu'il y a des vides entre deux tiles dans al mémoire vidéo, mais ne change rien à leur place en mémoire vidéo qui est prédéterminée, peu importe que la texture soit compressée ou non. L'intérêt de la compression de textures n'est pas de réduire la taille de la texture en mémoire vidéo, mais de réduire la quantité de données à lire/écrire en mémoire vidéo. Au lieu de lire T octets pour une tile non-compressée, on pourra en lire moins.
===La palette indicée et la technique de ''Vector quantization''===
La technique de compression des textures la plus simple est celle de la '''palette indicée''', que l'on a entraperçue dans le chapitre sur les cartes d'affichage. La technique de '''''vector quantization''''' peut être vue comme une amélioration de la palette, qui travaille non pas sur des texels, mais sur des ''tiles''. À l'intérieur de la carte graphique, on trouve une table qui stocke toutes les ''tiles'' possibles. Chaque ''tile'' se voit attribuer un numéro, et la texture sera composé d'une suite de ces numéros. Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l’embarqué utilisent ce genre de compression.
===Les algorithmes de ''Block Truncation coding''===
La première technique de compression élaborée est celle du '''''Block Truncation Coding''''', qui ne marche que pour les images en niveaux de gris. Le BTC ne mémorise que deux niveaux de gris par ''tile'', que nous appellerons couleur 1 et couleur 2, les deux niveaux de gris n'étant pas le même d'une ''tile'' à l'autre. Chaque pixel d'une ''tile'' est obligatoirement colorié avec un de ces niveaux de gris. Pour chaque pixel d'une ''tile'', on mémorise sa couleur avec un bit : 0 pour couleur 1, et 1 pour couleur 2. Chaque ''tile'' est donc codée par deux entiers, qui codent chacun un niveau de gris, et une suite de bits pour les pixels proprement dit. Le circuit de décompression est alors vraiment très simple, comme illustré ci-dessous.
[[File:Block Truncation coding.jpg|centre|vignette|upright=2.0|Block Truncation coding.]]
La technique du BTC peut être appliquée non pas du des niveaux de gris, mais pour chaque composante Rouge, Vert et Bleu. Dans ces conditions, chaque ''tile'' est séparée en trois sous-''tiles'' : un sous-bloc pour la composante verte, un autre pour le rouge, et un dernier pour le bleu. Cela prend donc trois fois plus de place en mémoire que le BTC pur, mais cela permet de gérer les images couleur.
===Le format de compression S3TC / DXTC===
L'algorithme de '''Color Cell Compression''', ou CCC, améliore le BTC pour qu'il gère des couleurs autre que des niveaux de gris. Ce CCC remplace les deux niveaux de gris par deux couleurs. Une ''tile'' est donc codée avec un entier 32 bits par couleur, et une suite de bits pour les pixels. Le circuit de décompression est identique à celui utilisé pour le BTC.
[[File:Color Cell Compression.jpg|centre|vignette|Color Cell Compression.]]
[[File:Dxt1-memory-layout.png|vignette|Dxt1 et ''color cell compression''.]]
Le format de compression de texture utilisé de base par Direct X, le DXTC, est une version amliorée de l'algorithme précédent. Il est décliné en plusieurs versions : DXTC1, DXTC2, etc. La première version du DXTC est une sorte d'amélioration du CCC : il ajoute une gestion minimale de transparence, et découpe la texture à compresser en ''tiles'' de 4 pixels de côté. La différence, c'est que la couleur finale d'un texel est un mélange des deux couleurs attribuée au bloc. Pour indiquer comment faire ce mélange, on trouve deux bits de contrôle par texel.
Si jamais la couleur 1 < couleur2, ces deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (2 * Couleur1 + Couleur2) / 3
* 11 = (Couleur1 + 2 * Couleur2) / 3
Sinon, les deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (Couleur1 + Couleur2) / 2
* 11 = Transparent
[[File:DXTC.jpg|centre|vignette|DXTC.]]
Le circuit de décompression du DXTC ressemble alors à ceci :
[[File:Circuit de décompression du DXTC.jpg|centre|vignette|upright=2.0|Circuit de décompression du DXTC.]]
===Les format DXTC 2, 3, 4 et 5 : l'ajout de la transparence===
Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3, lui-même replacé par le DXT4 et par le DXT5. Dans le DXT3, la transparence fait son apparition. Pour cela, on ajoute 64 bits par ''tile'' pour stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1.
[[File:Dxt23-memory-layout.png|centre|vignette|Dxt 2 et 3.]]
Dans le DXT4 et le DXT5, la méthode utilisée pour compresser les couleurs l'est aussi pour les valeurs de transparence. L'information de transparence est stockée par un en-tête contenant deux valeurs de transparence, le tout suivi d'une matrice qui attribue trois bits à chaque texel. En fonction de la valeur des trois bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1.
[[File:Dxt45-memory-layout.png|centre|vignette|Dxt 4 et 5.]]
===Le format de compression PVRTC===
Passons maintenant à un format de compression de texture un peu moins connu, mais pourtant omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle travaille surtout dans les cartes graphiques embarquées. Ses cartes se trouvent notamment dans l'ipad, l'iPhone, et bien d'autres smartphones actuels.
Avec le PVRTC, les textures sont encore une fois découpées en ''tiles'' de 4 texels par 4, mais la ressemblance avec le DXTC s’arrête là. Chacque ''tile'' est codée avec :
* une couleur codée sur 16 bits ;
* une couleur codée sur 15 bits ;
* 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
* et un bit de modulation, qui permet de configurer l’interprétation des bits de mélange.
Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4*4.
==Annexe : le ''normal-mapping'' hardware==
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, parlons un peu du ''normal mapping'' et de son implémentation dans les unités de texture. Pour rappel, 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. Et pour comprendre quel est le rapport avec les textures, nous allons devoir faire quelques rappels sur l'éclairage par pixel.
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
L'éclairage se fait en utilisant de nombreux calculs, qu'on a détaillé dans le chapitre sur les bases du rendu 3D. Ceux-ci utilisent la normale d'un sommet, à savoir un vecteur orienté à la verticale de la surface. En théorie, il y a une normale par sommet, ce qui fait que les calculs d'éclairage doivent se faire au niveau géométrique, avant la rastérisation. Mais pour obtenir un éclairage de meilleure qualité, il y a des techniques qui calcule l'éclairage d'une scène 3D pixel par pixel.
Une première technique d'éclairage par pixel interpole les normales lors de l'étape de rastérisation. On obtient alors un éclairage de Phong. Une autre solution est celle du ''normal mapping''. Le '''''normal mapping''''' précalcule les normales d'une surface dans une texture, appelée la ''normal map''. L'éclairage est alors réalisé par un pixel shader, qui lit les normales depuis cette texture et fait les calculs d'éclairage avec.
===L'usage de textures non-compressées pour les ''normal maps''===
La ''normal map'' est une texture, donc. Mais ce n'est pas une texture comme une autre. Elle mémorise un vecteur pour chaque texel, pas une couleur. Il est possible d'utiliser les formats de textures non-compressés, même si cela ne donne pas de bons résultats. Après tout, un vecteur est codé par trois coordonnées x,y,z, il est possible de coder chacune avec un octet et de packager cela dans une texture RGB classique. La coordonnée x va dans la composante Rouge, la coordonnée Y dans la composante Bleu, et la coordonnée z dans la composante z.
Une première optimisation est de ne pas mémoriser les trois coordonnées, mais seulement deux d'entre elles et de calculer la troisième. En effet, les normales sont des vecteurs ''normalisés'', c'est à dire que leur longueur vaut 1, par construction. Vu que la taille est connue à l'avance, on peut en déduire la coordonnée z à partir des coordonnées x et y, avec l'usage du théorème de Pythagore. Par définition, <math>z^2 = 1 - x^2 - y^2</math>. Le calcul peut être fait dans le ''pixel shader'' sans problème.
: Pour être précis, il faut que les normales soient définies d'une certaine manière pour que ça marche. Les normales ne sont pas définies dans le même système de coordonnées que le modèle 3D, mais dans un autre système appelé l'''espace tangent''.
Notons que l'accès à la ''normal map'' se fait comme pour n'importe quelle texture : les texels sont lus en mémoire vidéo, puis le filtrage de texture est appliqué, et enfin le tout est envoyé au ''pixel shader''. En théorie, le filtrage effectue une sorte d'interpolation des normales automatique, ce qui permet de trouver la normale à un pixel précis, même s'il n'est pas sur un texel.
Les solutions précédentes demandent d'utiliser des textures non-compressées, qui utilisent beaucoup de mémoire vidéo. Utiliser des textures compressées pourrait sembler résoudre le problème. Mais l'usage de textures compressées marche très mal pour les ''normal maps'', elles ne permettent pas d'obtenir une qualité ou une compression suffisante. Aussi, des formats de texture dédiés aux ''normal maps'' ont été inventés.
===Le format de texture 3Dc===
Le problème est que le rendu final n'est pas très beau. Et les opérations de filtrage ne donnent pas de très bons résultats. Pour remédier à ces problèmes, des formats de texture spécialisés pour les ''normal map'' ont été inventés. Le premier d'entre eux est le '''3Dc''', aussi appelé BC5 pour ''Block Compression 5''. Il utilise un octet par texel, au lieu de trois avec une texture RGB normale non-compressée.
Avec le 3Dc, seules coordonnées sont mémorisées, la troisième est calculée, comme dit plus haut. Pour le reste, la compression de la ''normal map'' ressemble un peu à celle des textures. La ''normal map'' est découpée en blocs de 4 par 4 texels de côté, chacune étant encodée en tenant compte de certaines redondances. Dans un bloc, les coordonnées x sont dans un certain intervalle, allant de <math>x_{min}</math> à <math>x_{max}</math>, idem pour la coordonnée y qui est dans l'intervalle <math>y_{min}</math>, <math>y_{max}</math>. L'intervalle <math>x_{min}</math> à <math>x_{max}</math> est coupé en 8 parts égales, ce qui fait 8 valeurs possibles dans cet intervalle, chacune correspondant à une valeur x possible pour une normale du bloc. La normale est donc encodée en précisant quelle valeur est la bonne, en utilisant un indice de 3 bits pour cela. La valeur exacte de la coordonnée x se calcule à partir de l'indice comme suit :
: <math>x = x_{min} + \left( \frac{x_{min} - x_{min}}{8} \right) \times \text{indice} </math>
Pour encoder un bloc de 4 par 4 normales, il faut donc :
* Un octet pour chaque coordonnée <math>x_{min}</math>, <math>x_{max}</math>, <math>y_{min}</math>, <math>y_{max}</math>.
* 6 bits par normale : 3 pour encoder le décalage de la coordonnée x, 3 pour la coordonnée y.
Le tout permet d'encoder un bloc de 16 normales en seulement 128 bits, soit un octet par normale. Le ''pixel shader'' se débrouille pour décompresser un bloc. Il effectue notamment le calcul du dessus, pour retrouver la coordonnée x. Les calculs étant particulièrement simples, le cout en performance est très faible. Et ce d'autant plus qu'ils demandent juste de faire des additions et des multiplications entières, que l'unité scalaire peut faire en parallèle d'autres opérations plus gourmandes. Et le cout en calculs est de toute façon compensé par l'économie de mémoire et de bande passante lié à la compression : diviser la taille des données par trois, ca a un sacré impact !
==Annexe : les ''shadowmap'' hardware==
Les anciens GPU, notamment la Geforce FX, avaient des fonctionnalités spécifiques pour le calcul des ombres. Dans la plupart des jeux vidéos de l'époque, et même de maintenant, les ombres sont calculées avec la technique des ''shadowmap''. L'idée est assez simple sur le principe : un pixel est dans l'ombre quand il est invisible depuis une source de lumière.
L'idée est que le rendu est réalisé en plusieurs passes, avec une passe par source de lumière et une passe finale pour calculer l'image finale. Nous allons expliquer la technique avec une seule source de lumière, et allons utiliser l'exemple de la scène ci-dessous.
[[File:7fin.png|centre|vignette|Scène 3D d'exemple.]]
===La technique du ''shadowmapping''===
[[File:2shadowmap.png|vignette|Résultat de la première passe : ''shadowmap''..]]
La première passe rend l'image depuis le point de vue de la source de lumière. Cette première passe ne rend pas les couleurs de la scène, elle ne s'intéresse qu'à la profondeur des pixels. Le résultat est que l'image ne rend que le tampon de profondeur. Celui-ci est ensuite réutilisé comme texture pour la passe suivante. La texture en question est appelée la '''''shadownmap'''''.
La perspective utilisée, ainsi que le ''view frustrum'', dépend de la source de lumière. Pour une source de lumière qui émet un cône de lumière, le ''view frustrum'' de l'image rendue doit contenir tout le cone de lumière, et doit coller le plus possible à celui-ci. Pour une source directionnelle, comme le soleil, une perspective orthographique est utilisée.
La seconde passe rend l'image du point de vue de la caméra, pour rendre l'image finale. Elle rend l'image finale, qui est composée de pixels, chacun ayant une position à l'écran x,y, et une profondeur z. Les coordonnées sont transformées pour obtenir la position de ce pixel depuis le point de vue de la caméra. Une simple multiplication de matrice suffit, rien de bien compliqué, un shader peut le faire.
[[File:5failed.png|vignette|Résultat du test des comparaisons.]]
Après cette étape, on a alors les coordonnées x,y,z de ce pixel du point de vue de la caméra, et la ''shadowmap''. Il est alors possible d'accéder à la ''shadowmap'' au même endroit, à la même place que le pixel testé, aux mêmes coordonnées x,y. Si la profondeur du pixel est supérieure à celle de la shadowmap au même endroit, alors le pixel est situé derrière la surface visible, donc est dans l'ombre. Sinon, il n'est pas dans l'ombre. Le même procédé est répété sur chaque pixel de l'écran.
===Les optimisations hardware du ''shadowmapping''===
La technique des ''shadowmap'' demande donc de calculer une texture ''shadowmap'', puis de lire celle-ci et de faire des comparaisons de profondeur. Les GPU comme la Geforce FX intégraient du matériel dans les unités de texture pour faciliter ce travail. Les unités de texture pouvaient lire les ''shadowmap'', et faire la comparaison de profondeur toutes seules, elles avaient des circuits pour. Il suffisait de leur fournir le pixel à tester, ses coordonnées x,y,z, et l'adresse de la ''shadowmap''. Les unités de texture renvoyaient alors un résultat valant 0 ou 1 : 1 si le pixel est dans l'ombre, 0 sinon.
Elles pouvaient même effectuer du filtrage de texture sur les ''shadowmap''. Mais le filtrage était différent de celui utilisé sur les autres textures : moyenner des valeurs de profondeur ne marche pas bien. Elles utilisaient des techniques de filtrage différentes : elles faisaient les tests de comparaison, puis faisaient la moyenne des résultats. Ainsi, pour du filtrage bilinéaire, elles lisaient 4 texels dans la ''shadowmap'', puis faisaient 4 tests de comparaison, et moyennaient les 4 résultats.
==Annexe : les textures virtuelles==
Les '''textures virtuelles''' sont une optimisation des textures normales, qui visent à accélérer le rendu de terrains de grande taille. Imaginez par exemple un monde assez ouvert, comme un environnement en forêt ou en montagne, avec une grande distance de visibilité. Avec de tels terrains, le "sol" est recouvert par une texture de sol unique qui recouvre tout le terrain. Elle ne se répète pas, est de très grande taille, et peut parfois recouvrir toute la map ! Mais il n'y a pas assez de mémoire vidéo pour mémoriser la texture toute entière. La seule solution est la suivante : une partie de la texture est placée en mémoire vidéo, le reste est soit placé en mémoire RAM ou sur le disque dur.
Pour cela, le moteur de jeu utilise une optimisation ingénieuse, basée sur une observation assez basique : une bonne partie de la texture est visible, mais le reste est caché par des arbres, des habitations ou d'autres obstacles. Une optimisation possible de ne garder en mémoire vidéo que les portions visibles de la texture, pas les portions cachées. Une autre optimisation mélange textures virtuelles et ''mip-mapping''. L'idée est que pour les portions lointaines d'une texture, la texture utilisée est une ''mip-map'' de basse résolution. L'idée est alors de ne charger que la ''mip-map'' adéquate, pas les autres niveaux de détail. En clair, la texture de base n'est pas chargée en mémoire vidéo, mais la ''mip-map'' basse résolution l'est.
===Une texture à deux niveaux===
L'implémentation des textures virtuelles découpe les méga-textures en ''tiles'', en morceaux rectangulaires de taille modeste. En clair, le terrain est découpé en morceau rectangulaires/carrés. Seules les tiles nécessaires sont chargées en mémoire vidéo, pas les autres. Par exemple, les ''tiles'' non-visibles ne sont pas placées en mémoire vidéo, seules les ''tiles'' visibles le sont. De même, il y a une ''tile'' par niveau de mip-map : seul la tile correspondant le niveau adéquat est en mémoire vidéo, les autres niveaux de détail ne sont pas chargés. On peut faire une analogie avec la mémoire virtuelle, où les données sont découpées en pages, qui sont chargées en mémoire RAM à la demande, suivant les besoins, les données pouvant être swappées sur le disque dur si elles sont peu utilisées. Sauf qu'ici, il s'agit de textures qui sont découpées en pages chargées à la demande en mémoire vidéo, depuis la RAM système.
Une texture virtuelle est en réalité un système à deux niveaux : une liste de ''tiles'' et les ''tiles'' elles-mêmes. La liste de ''tiles'' est appelée un '''atlas de texture''', c'est un peu l'équivalent de la ''tilemap'' pour le rendu 2D. Rendre une texture demande de calculer quelle ''tile'' contient le texel à afficher, consulter la ''tile'' en question, puis récupérer le texel adéquat dans cette ''tile''. La ''tile'' est donc une texture, mais la texture à charger est choisie parmi un ensemble, qui est ici l'atlas de texture.
===L'implémentation : logicielle versus matérielle===
Les textures virtuelles ont été utilisées pour la première fois par les jeux Rage 1 et 2 d'IdSoftware, et quelques jeux ultérieurs comme DOOM 2016. IdSoftware les appelait des '''''mega-textures'''''. L'optimisation permettait des gains en performance assez impressionnants. Le jeu Rage 1 utilisait une texture carrée unique de 128k pixels de côté pour rendre le terrain. En théorie, une telle texture devrait prendre 64 giga-octets, mais le jeu tournait correctement avec 512 méga-octets de RAM, poussivement avec seulement 256 méga-octets de RAM.
De nos jours, les textures virtuelles sont supportées par beaucoup de jeux vidéos, les moteurs les plus courants gèrent de telles textures de manière logicielles. Mais quelques GPU récents supportent les textures virtuelles. Sur les GPU récents, l'atlas de texture est géré nativement par le matériel. Le GPU choisit quelle ''tile'', quelle texture choisir pour rendre le texel adéquat. Pour cela, le GPU calcule quelle ''tile'' charger, consulte l'atlas de texture, et lit la texture de ''tile'' adéquate.
Mais l'implémentation sur les GPU récents a de nombreuses limitations. La limitation la plus importante est que la taille des textures virtuelles ne peut pas dépasser la taille d'une texture normale, soit 32768 pixels de côté pour une texture carrée environ sur les GPU de 2020. De plus, le chargement d'une ''tile'' est très lent. En clair, dès qu'on veut changer de niveau de mip-map pour une tile, ou dès qu'une tile devient visible, le chargement de la tile peut facilement prendre plusieurs centaines de millisecondes. Le filtrage de texture est très complexe avec des textures virtuelles, ce qui fait que le filtrage de texture virtuelle est souvent soumis à des limitations que les textures normales n'ont pas, notamment pour le filtrage anisotropique.
{{NavChapitre | book=Les cartes graphiques
| prev=Le rasterizeur
| prevText=Le rasterizeur
| next=Les Render Output Target
| nextText=Les Render Output Target
}}{{autocat}}
ezr3kw3h0md2f4p8un1kk4e7c12b9vl
763418
763417
2026-04-10T18:50:58Z
Mewtow
31375
/* Le format de texture 3Dc */
763418
wikitext
text/x-wiki
[[File:Texture mapping.png|vignette|''Texture mapping'']]
Les '''textures''' sont des images que l'on va plaquer sur la surface d'un objet, du papier peint en quelque sorte. Les cartes graphiques supportent divers formats de textures, qui indiquent comment les pixels de l'image sont stockés en mémoire : RGB, RGBA, niveaux de gris, etc. Une texture est donc composée de "pixels", comme toute image numérique. Pour bien faire la différence entre les pixels d'une texture, et les pixels de l'écran, les pixels d'une texture sont couramment appelés des ''texels''.
==Le placage de textures inverse==
Pour rappel, plaquer une texture sur un objet consiste à attribuer un texel à chaque sommet, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture.
Dans les faits, on n'utilise pas de coordonnées entières de ce type. Les coordonnées de texture sont deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. Le nom donnée à cette technique de description des coordonnées de texture s'appelle l''''''UV Mapping'''''.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Les API 3D modernes gèrent des textures en trois dimensions, ce qui ajoute une troisième coordonnée de texture notée w. Dans ce qui va suivre, nous allons passer les textures en trois dimensions sous silence. Elles ne sont pas très utilisées, la quasi-totalité des jeux vidéo et applications 3D utilisant des textures en deux dimensions. Par contre, le matériel doit gérer les textures 3D, ce qui le rend plus complexe que prévu. Il faut ajouter quelques circuits pour, de quoi gérer la troisième coordonnée de texture, etc.
Lors de la rastérisation, chaque fragment se voit attribuer un sommet, et donc la coordonnée de texture qui va avec. Si un pixel est situé pile sur un sommet, la coordonnée de texture de ce sommet est attribuée au pixel. Si ce n'est pas le cas, la coordonnée de texture finale est interpolée à partir des coordonnées des trois sommets du triangle rastérisé. L'interpolation en question a lieu dans l'étape de rastérisation, comme nous l'avons vu dans le chapitre précédent. Le fait qu'il y ait une interpolation fait que les coordonnées du pixel gagent à être des nombres flottants. On pourrait faire une interpolation avec des coordonnées de texture entières, mais les arrondis et autres imprécisions de calcul donneraient un résultat graphiquement pas terrible, et empêcheraient d'utiliser les techniques de filtrage de texture que nous verrons dans ce chapitre.
À partir de ces coordonnées de texture, la carte graphique calcule l'adresse du texel qui correspond, et se charge de le lire. Et toute la magie a lieu dans ce calcul d'adresse, qui part de coordonnées de texture flottante, pour arriver à une adresse mémoire. Le calcul de l'adresse du texel se fait en plusieurs étapes, que nous allons voir ci-dessous. La première étape convertit les coordonnées flottantes en coordonnées entières, qui disent à quel ligne et colonne se trouve le texel voulu dans la texture. L'étape suivante transforme ces coordonnées x,y entières en adresse mémoire.
===La normalisation des coordonnées===
J'ai dit plus haut que les coordonnées de texture sont des coordonnées flottantes, comprises entre 0 et 1. Mais il faut savoir que les pixels shaders peuvent modifier celles-ci pour mettre en œuvre certains effets graphiques. Et le résultat peut alors se retrouver en-dehors de l'intervalle 0,1. C'est quelque chose de voulu et qui est traité par la carte graphique automatiquement, sans que ce soit une erreur. Au contraire, la manière dont la carte graphique traite cette situation permet d'implémenter des effets graphiques comme des textures en damier ou en miroir.
[[File:Clamp tile.jpg|vignette|Clamp tile]]
Il existe globalement trois méthodes très simples pour gérer cette situation, qui sont appelés des '''modes d'adressage de texture'''.
* La première méthode est de faire en sorte que le résultat sature. Si une coordonnée est inférieur à 0, alors on la remplace par un zéro. Si elle est supérieure à 1, on la ramène à 1. Avec cette méthode, tout se passe comme si les bords de la texture étaient étendus et remplissaient tout l'espace autour de la texture. Le tout est illustré ci-dessous. Ce mode d'accès aux textures est appelé le '''''clamp'''''.
* Une autre solution retire la partie entière de la coordonnée, elle coupe tout ce qui dépasse 1. Pour le dire autrement, elle calcule le résultat modulo 1 de la coordonnée. Le résultat est que tout se passe comme si la texture était répétée à l'infini et qu'elle pavait le plan.
* Une autre méthode remplit les coordonnées qui sortent de l’intervalle 0,1 avec une couleur préétablie, configurée par le programmeur.
===La conversion des coordonnées de textures flottantes en adresse mémoire===
Une fois la normalisation effectuée, les coordonnées de texture sont utilisées pour lire le texel voulu. Pour cela, les coordonnées de texte sont transformées en adresse mémoire, adresse qui pointe sur le texel ayant ces cordonnées. Pour cela, la première étape est de transformer les coordonnées flottantes u,v en coordonnées entières x,y qui pointent sur un texel. Pour cela, il suffit de multiplier les coordonnées flottantes u,v par la résolution de la texture accédée. Pour un écran de résolution <math>\text{height,width}</math>, le calcul est le suivant :
: <math>x = u \times \text{width}</math>
: <math>y = v \times \text{height}</math>
Le résultat est un nombre avec une partie entière et une partie fractionnaire. La partie entière des deux coordonnées donne la position x,y voulue, et la partie fractionnaire est conservée pour le filtrage de textures, mais passons cela sous silence pour le moment.
La seconde étape prend les coordonnées entières x,y et calcule l'adresse mémoire du texel. L'adresse dépend de la position de la texture en mémoire, précisément de son début, son premier texel, mais aussi de la position du texel par rapport au début de la texture. Et calculer cette position intra-texture dépend de la manière dont les texels sont stockés en mémoire.
====Les textures naïves====
Les programmeurs qui lisent ce cours s'attendent certainement à ce que la texture soit stockée en mémoire ligne par ligne, ou colonne par colonne. Cela veut dire que le premier pixel en partant d'en haut à gauche est stocké en premier, puis celui immédiatement à sa droite, puis celui encore à droite, et ainsi de suite. Une fois qu'on arrive à la fin d'une ligne, on passe à la ligne suivante, en-dessous. Cette organisation ligne par ligne s'appele l'organisation '''''row major order'''''. On peut faire pareil, mais colonne par colonne, ce qui donne le '''''column major order'''''.
[[File:Speicheranordnung Feld.svg|centre|vignette|upright=2|Row et column major order.]]
Maintenant, supposons que la texture commence à l'adresse <math>A_\text{texture}</math>, qui est l'adresse du premier texel. La texture a une résolution de <math>\text{width}</math> texels de large et <math>\text{height}</math> texels de haut. Par définition, les coordonnées X et Y des texels commencent à 0, ce qui fait que le pixel en haut à gauche a les coordonnées 0,0.
L'adresse du pixel se calcule comme suit :
: <math>A_\text{pixel} = A_\text{texture} + (\text{taille d'une ligne en octets} \times Y) + (\text{taille d'un texel en octets} \times X)</math>
La taille d'un pixel en mémoire est notée T. La taille d'une ligne en mémoire est de <math>width \times T</math>, par définition, vu qu'elle fait <math>width</math> texels. On a donc :
: <math>A_\text{pixel} = A_\text{texture} + (width \times T \times Y) + (T \times X)</math>
La formule se réécrit comme suit :
: <math>A_\text{pixel} = A_\text{texture} + T \times (width \times Y + X)</math>
Le calcul d'adresse est donc assez simple. Malheureusement, les textures ne sont pas stockées de cette manière en mémoire vidéo. En effet, elle se marie mal avec les opérations de filtrage de texture que nous allons voir dans ce qui suit. Le filtrage d'un texel dépend de ses voisins du dessus et du dessous. Le fait que la texture n'est pas forcément parcourue ligne par ligne fait que stocker une texture ligne par ligne n'est pas l'idéal.
De même, les textures sont déformées par la perspective. L'affichage de la texture ne se fait alors pas ligne par ligne, mais en parcourant la texture en diagonale, l'angle de la diagonale correspondant approximativement à l'angle que fait la verticale de la texture avec le regard. Vu qu'on ne connait pas à l'avance l'angle que fera la diagonale de parcours, on doit ruser.
====Les textures tilées====
Une première solution à ce problème est celle des '''textures tilées'''. Avec ces textures, l'image de la texture est découpée en ''tiles'', des rectangles ou en carrés de taille fixe, généralement des carrés de 4 pixels de côté. Les tiles ont une largeur et une longueur égales, afin de simplifier les calculs : on divise X et Y par le même nombre. De plus, leur largeur et leur longueur sont une puissance de deux, afin de simplifier les calculs d'adresse. Les ''tiles'' sont alors mémorisée les unes après les autres dans le fichier de la texture.
[[File:Texture tilée.png|centre|vignette|upright=2|Texture tilée]]
La formule de calcul d'adresse vue plus haut doit être adaptée pour tenir compte des tiles. Pour cela, il faut remplacer la taille d'un texel par la taille d'une tile, et que la largeur de la texture soit exprimée en nombre de tiles. De plus, on doit adapter les coordonnées des texels pour donner des coordonnées de tile. Généralement, les tiles sont des carrés de N pixels de côté, ce qui fait qu'on peut regrouper les lignes et les colonnes par paquets de N. Il suffit donc de diviser Y et X pour obtenir les coordonnées de la tile, de même que la larguer. La formule pour calculer la position de la énième tile est alors la suivante :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + \text{Taille mémoire d'une tile} \times \left( {\text{Width} \over N} \times {Y \over N} + {X \over N} \right)</math>
On peut réécrire le tout comme suit :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + K \times \left( {Y \over N} + X \right)</math>, avec K une constante connue à la compilation des shaders.
Vu que les tiles sont carrées avec une largeur qui est une puissance de deux, la multiplication par la taille d'une tile en mémoire se simplifie : on passe d'une multiplication entière à des décalages de bits. Même chose pour le calcul de l'adresse de la tile à partir des coordonnées x,y : ils impliquent des divisions par une puissance de deux, qui deviennent de simples décalages.
La position d'un pixel dans une tile dépend du format de la texture, mais peut se calculer avec quelques calculs arithmétiques simples. Dans les cas les plus simples, les pixels sont mémorisés ligne par ligne, ou colonne par colonne. Mais ce n'est pas systématiquement le cas. Toujours est-il que les calculs pour déterminer l'adresse sont simples, et ne demandent que quelques additions ou multiplications. Mais avec les formats de texture utilisés actuellement, les tiles sont chargées en entier dans le cache de texture, sans compter que diverses techniques de compression viennent mettre le bazar, comme on le verra dans la suite de cours.
Un avantage de l'organisation en tiles est qu'elle se marie bien avec le parcours des textures. On peut parcourir une texture dans tous les sens, horizontal, vertical, ou diagonal, on sait que les prochains pixels ont de fortes chances d'être dans la même tile. Si on rentre dans une tile par la gauche en haut, on a encore quelques pixels à parcourir dans la tile, par exemple. De même, le filtrage de textures est facilité. On verra dans ce qui va suivre que le filtrage de texture a besoin de lire des blocs de 4 texels, des carrés de 2 pixels de côté. Avec l'organisation en tile, on est certain que les 4 texels seront dans la même tile, sauf s'ils ont le malheur d'être tout au bord d'une tile. Ce dernier cas est assez rare, et il l'est d'autant plus que les tiles sont grandes. Enfin, un dernier avantage est que les tiles sont généralement assez petites pour tenir tout entier dans une ligne de cache. Le cache de texture est donc utilisé à merveille, ce qui rend les accès aux textures plus rapides.
====Les textures basées sur des ''z-order curves''====
Les formats de textures théoriquement optimaux utilisent une '''''Z-order curve''''', illustrée ci-dessous. L'idée est de découper la texture en quatre rectangles identiques, et de stocker ceux-ci les uns à la suite des autres. L'intérieur de ces rectangles est lui aussi découpé en quatre rectangles, et ainsi de suite. Au final, l'ordre des pixels en mémoire est celui illustré ci-dessous.
[[File:Z-CURVE.svg|centre|vignette|upright=2|Construction d'une ''Z-order curve''.]]
Les texels sont stockés les uns à la suite des autres dans la mémoire, en suivant l'ordre donnée par la ''Z-order curve''. Le calcul d'adresse calcule la position du texel en mémoire, par rapport au début de la texture, et ajoute l'adresse du début de la texture. Mais tout le défi est de calculer la position d'un texel en mémoire, à partir des coordonnées x,y. Le calcul peut sembler très compliqué, mais il n'en est rien. Le calcul demande juste de regarder les bits des deux coordonnées et de les combiner d'une manière particulièrement simple. Il suffit de placer le bit de poids fort de la coordonnée x, suivi de celui de la coordonnée y, et de faire ainsi de suite en passant aux bits suivants.
[[File:Zcurve45bits.png|centre|vignette|upright=1.5|Calcul de la position d'un élément dans une ''Z-order curve'' à partir des coordonnées x et y.]]
L'avantage d'une telle organisation est que la textures est découpées en ''tiles'' rectangulaires d'une certaine taille, elles-mêmes découpées en ''tiles'' plus petites, etc. Et il se trouve que cette organisation est parfaite pour le cache de texture. L'idéal pour le cache de texture est de charger une ''tile'' complète dans le cache de textures. Quand on accède à un texel, on s'assure que la ''tile'' complète soit chargée. Mais cela demande de connaitre à l'avance la taille d'une ''tile''. Les formats de texture fournissent généralement une ''tile'' carré de 4 pixels de côté, mais cela donnerait un cache trop petit pour être vraiment utile. Avec cette méthode, on s'assure qu'il y ait une ''tile'' avec la taille optimale. Les ''tiles'' étant découpées en ''tiles'' plus petites, elles-mêmes découpées, et ainsi de suite, on s'assure que la texture est découpées en ''tiles'' de taille variées. Il y aura au moins une ''tile'' qui rentrera tout pile dans le cache.
==Les techniques de rendu à textures multiples==
Nous venons de voir comment une texture est plaquée sur un objet 3D, ou une surface comme un sol. Pour résumer, le calcul de l'adresse d'un texel prend la position du texel par rapport au début de la texture, et ajoute l'adresse du début de la texture. L'adresse mémoire de la texture est connue au moment où le pilote de la carte graphique place la texture dans la mémoire vidéo, et cette information est transmise au matériel par l'intermédiaire du processeur de commande, puis passée aux processeurs de shaders et à l'unité de texture. Le tout est couplé à d'autres informations, la plus importante étant la ''taille de la texture en octets'', pour éviter de déborder lors des accès à la texture.
Néanmoins, il s'agit là du cas le plus simple. Certaines techniques de rendu demandent de choisir la texture à plaquer parmi un ensemble de plusieurs textures. Les techniques en question sont assez variées et n'ont pas grand chose en commun. Les plus connues sont le ''mip-mapping'', le ''cube-mapping'' et les textures virtuelles. Le ''mip-mapping'' sert à filtrer les textures, chose qu'on expliquera plus tard, le ''cube-mapping'' sert à simuler des réflexions sur un objet en plaquant une texture de l'environnement dessus, les textures virtuelles sont une optimisation pour les textures des terrains de grande taille. Mais malgré leurs différences, elles demandent de choisir quelle texture plaquer entre plusieurs textures de base. En clair, l'adresse de base de la texture varie selon la situation. Voyons-les dans le détail.
===Le mip-mapping===
Le '''mip-mapping''' a pour but de légèrement améliorer les graphismes des objets lointains, tout en rendant les calculs de texture plus rapides. Formellement, le ''mip-mapping'' est une technique de filtrage de texture, mais nous l'abordons maintenant car elle est surtout liée au calcul d'adresse. Les unités de texture ont des circuits de filtrage de texture séparés des circuits de ''mip-mapping'' et de calcul d'adresse, d'où le fait que nous en parlons séparément.
Le problème résolu par le ''mip-mapping'' est le rendu des textures lointaines. Si une texture est plaquée sur un objet lointain, une bonne partie des détails est invisible pour l'utilisateur. Un pixel de l'écran est associé à plusieurs texels. Idéalement, la carte graphiques devrait lire tous ces texels et en faire une sorte de moyenne pondérée, pour calculer la couleur finale du pixel. Mais dans les faits, ce serait très gourmand et compliqué à implémenter en hardware. Une solution serait de ne garder que quelque texels, mais cela a tendance à créer des artefacts visuels (les textures affichées ont tendance à pixeliser). Le ''mip-mapping'' permet de réduire ces deux problèmes en même temps en précalculant cette moyenne pondérée pour des distances prédéfinies.
L'idée est d'utiliser plusieurs exemplaires d'une même texture à des résolutions différentes, chaque exemplaire étant adapté à une certaine distance. Par exemple, une texture sera stocké avec un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre de 128 * 128 et ainsi de suite jusqu’à un dernier exemplaire de 32 * 32 pixel. Chaque exemplaire correspond à un '''niveau de détail''', aussi appelé ''Level Of Detail'' (abrévié en LOD). La résolution utilisée diminue d'autant plus que l'objet est situé loin de la caméra. Les objets proches seront rendus avec la texture 512*512, ceux plus lointains seront rendus avec la texture de résolution 256*256, les textures 128*128 seront utilisées encore plus loin, et ainsi de suite jusqu'aux objets les plus lointains qui sont rendus avec la texture la plus petite de 32*32.
[[File:MipMap Example STS101.jpg|centre|vignette|upright=2|Exemples de mip-maps.]]
Le ''mip-mapping'' améliore grandement la qualité d'image. L'image d'exemple ci-dessous le montre assez bien.
[[File:Mipmapping example.png|centre|vignette|upright=2|Exemple de mipmapping.]]
Pour faciliter les calculs d'adresse, les LOD d'une même texture sont stockées les uns après les autres en mémoire (dans un tableau, comme diraient les programmeurs). Ainsi, pas besoin de se souvenir de la position en mémoire de chaque LOD : l'adresse de la texture de base, et quelques astuces arithmétiques suffisent. Prenons le cas où la texture de base a une taille L. le premier exemplaire est à l'adresse 0, le second niveau de détail est à l'adresse L, le troisième à l'adresse L + L/4, le suivant à l'adresse L + L/4 + L/16, et ainsi de suite. Le calcul d'adresse demande juste connaître le niveau de détails souhaité et l'adresse de base de la texture. Le niveau de détail voulu est calculé par les pixel shaders, en fonction de la coordonnée de profondeur du pixel à traiter.
Évidemment, cette technique consomme de la mémoire vidéo, vu que chaque texture est dupliquée en plusieurs exemplaires, en plusieurs LOD. Dans le détail, la technique du mip-mapping prend au maximum 33% de mémoire en plus (sans compression). Cela vient du fait qu'en prenant une texture dexu fois plus petite, elle prend 4 fois moins de mémoire : 2 fois moins de pixels en largeur, et 2 fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des LODs, texture de base comprise, prendra X + (X/4) + (X/16) + (X/256) + … Un petit calcul de limite donne 4/3 * X, soit 33% de plus.
===Le cube-mapping===
[[File:Cube mapped reflection example 2.JPG|vignette|Exemple de reflets environnementaux.]]
L''''environnement-mapping''' est une technique de calcul de divers effets graphiques liés à l'environnement, notamment des réflexions. L'idée est de plaquer une texture pré-calculée pour simuler l'effet de l'environnement sur une surface ou un objet 3D. Il en existe plusieurs versions différentes, mais la seule utilisée de nos jours est le ''cube-mapping'', où la texture de l'environnement est plaquée sur un cube, d'où son nom. Le cube en question est utilisé différemment suivant ce que l'on cherche à faire avec le ''cube-mapping''. Les deux utilisations principales sont le rendu du ciel et des décors, et les réflexions sur la surface des objets. Dans les deux cas, l'idée est de précalculer ce que l'on voit du point de vue de la caméra. On place la caméra dans la scène 3D, on place un cube centré sur la caméra, le cube est texturé avec ce que l'on voit de l'environnement depuis la caméra/l'objet de son point de vue.
[[File:Panorama cube map.png|centre|vignette|upright=2|L'illustration montre en premier lieu une ''cubemap'' avec les six faces mises en évidence, puis quel environnement 3D elle permet de simuler, le troisième illustration montrant comment la ''cubemap'' est utilisée pour simuler l'environnement.]]
Le rendu du ciel et des décors lointains dans les jeux vidéo se base sur des '''''skybox''''', à savoir un cube centré sur la caméra, sur lequel on ajoute des textures de ciel ou de décors lointains. Le cube est recouvert par une texture, qui correspond à ce que l'on voit quand on dirige le regard de la caméra vers cette face. Contrairement à ce qu'on pourrait croire, la skybox n'est pas les limites de la scène 3D, les limites du niveau d'un jeu vidéo ou quoique ce soit d'autre de lié à la physique de la scène 3D. La skybox est centrée sur la caméra, elle suit la caméra dans son mouvement. Centrer la skybox sur la caméra permet de simuler des décors très lointains, suffisamment lointain pour qu'on n'ait pas l'illusion de s'en rapprocher en se déplaçant dans la map. De plus, cela évite d'avoir à faire trop de calculs à chaque fois que l'on bouge la caméra. La texture plaquée sur le cube est une texture unique, elle-même découpée en six sous-textures, une par face du cube.
[[File:Skybox example.png|centre|vignette|upright=2|Exemple de Skybox.]]
[[File:Cube mapped reflection example.jpg|vignette|Réflexions calculées par une ''cubemap''.]]
Le ''cube-mapping'' est aussi utilisé pour des reflets. L'idée est de simuler les reflets en plaquant une texture pré-calculée sur l'objet réflecteur. La texture pré-calculée est un dessin de l'environnement qui se reflète sur l'objet, un dessin du reflet à afficher. En la plaquant la texture sur l'objet, on simule ainsi des reflets de l'environnement, mais on ne peut pas calculer d'autres reflets comme les reflets objets mobiles comme les personnages. Et il se trouve que la texture pré-calculée est une ''cubemap''. Pour les environnements ouverts, c'est la ''skybox'' qui est utilisée, ce qui permet de simuler les reflets dans les flaques d'eau ou dans des lacs/océans/autres. Pour les environnements intérieurs, c'est une cubemap spécifique qui utilisée. Par exemple, pour l'intérieur d'une maison, on a une ''cubemap'' par pièce de la maison. Les reflets se calculent en précisant quelle ''cubemap'' appliquer sur l'objet en fonction de la direction du regard.
[[File:Cube map level.png|centre|vignette|Cube map de l'intérieur d'une pièce d'un niveau de jeux vidéo.]]
Toujours est-il que les textures utilisées pour le ''cubemmapping'', appelées des ''cubemaps'', sont en réalité la concaténation de six textures différentes. En mémoire vidéo, la ''cubemap'' est stockée comme six textures les unes à la suite des autres. Lors du rendu, on doit préciser quelle face du cube utiliser, ce qui fait 6 possibilités. On a le même problème qu'avec les niveaux de détail, sauf que ce sont les faces d'une ''cubemap'' qui remplacent les textures de niveaux de détails. L'accès en mémoire doit donc préciser quelle portion de la ''cubemap'' il faut accéder. Et l'accès mémoire se complexifie donc. Surtout que l'accès en question varie beaucoup suivant l'API graphique utilisée, et donc suivant la carte graphique.
Les API 3D assez anciennes ne gérent pas nativement les ''cubemaps'', qui doivent être émulées en logiciel en utilisant six textures différentes. Le pixel shader décide donc quelle ''cubemap'' utiliser, avec quelques calculs sur la direction du regard. L'accès se fait d'une manière assez simple : le shader choisit quelle texture utiliser. Les API 3D récentes gèrent nativement les ''cubemaps''. Dans le cas le plus simple,pour les versions les plus vielles de ces API, les six faces sont numérotées et l'accès à une ''cubemap'' précise quel face utiliser en donnant son numéro. La carte graphique choisit alors automatiquement la bonne texture, mais cela demande de laisser le calcul de la bonne face au pixel shader. D'autres API 3D et cartes graphiques font autrement. Dans les API 3D modenres, les ''cubemap'' sont gérées comme des textures en trois dimensions, adressées avec trois coordonnées u,v,w. La carte graphique utilise ces trois coordonnées de manière à en déduire quelle est la face pertinente, mais aussi les coordonnées u,v dans la texture de la face.
==L'implémentation matérielle du placage de textures==
Pour résumer, la lecture d'un texel demande d'effectuer plusieurs étapes. Dans le cas le plus simple, sans ''mip-mapping'' ou ''cubemapping'', on doit effectuer les étapes suivantes :
* Il faut d'abord normaliser les coordonnées de texture pour qu'elles tombent dans l'intervalle [0,1] en fonction du mode d'adressage désiré.
* Ensuite, les coordonnées u,v doivent être converties en coordonnées entières, ce qui demande une multiplication flottante.
* Enfin, l'adresse finale est calculée à partir des coordonnées entières et en ajoutant l'adresse de base de la texture (et éventuellement avec d'autres calculs arithmétiques suivant le format de la texture).
Tout cela pourrait être fait par le pixel shaders, mais cela implique beaucoup de calculs répétitifs et d'opérations arithmétiques assez lourdes, avec des multiplications flottantes, des additions et des multiplications entières, etc. Faire faire tous ces calculs par les shaders serait couteux en performance, sans compter que les shaders deviendraient plus gros et que cela aurait des conséquences sur le cache d'instruction. De plus, certaines de ces étapes peuvent se faire en parallèle, comme les deux premières, ce qui colle mal avec l'aspect sériel des shaders.
Aussi, les processeurs de shaders incorporent une unité de calcul d'adresse spéciale pour faire ces calculs directement en matériel. L'unité de texture contient au minimum deux circuits : un circuit de calcul d'adresse, et un circuit d'accès à la mémoire. Toute la difficulté tient dans le calcul d'adresse, plus que dans le circuit de lecture. Le calcul d'adresse est conceptuellement réalisé en deux étapes. La première étape qui transforme les coordonnées u,v en coordonnées x,y qui donne le numéro de la ligne et de la colonne du texel dans la texture. La seconde étape prend ces deux coordonnées x,y, l'adresse de la texture, et détermine l'adresse de la tile à lire.
[[File:Unité de texture simple.png|centre|vignette|upright=2|Unité de texture simple]]
===L'implémentation du mip-mapping===
Le ''mip-mapping'' est lui aussi pris en charge par l'unité de calcul d'adresse, car cette technique change l'adresse de base de la texture. La gestion du ''mip-mapping'' est cependant assez complexe. Il est possible de laisser le pixel shader calculer quel niveau de détail utiliser, en fonction de la coordonnée de profondeur z du pixel à afficher. La carte graphique détermine alors automatiquement quelle texture lire, quel niveau de détail, automatiquement. Elle détermine aussi la bonne résolution pour la texture, qui est égal à la résolution de la texture de base, divisée par le niveau de détail. Pour résumer, le niveau de détail est envoyé aux unités de texture, qui s'occupent de calculer l'adresse de base et la résolution adéquates. Quelques calculs arithmétiques simples, donc, qui s'implémentent facilement avec quelques circuits.
Mais une autre méthode laisse la carte graphique déterminer le niveau de détail par elle-même. Dans ce cas, cela demande, outre les deux coordonnées de texture, de calculer la dérivée de ces deux coordonnées dans le sens horizontal et vertical, ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Les quatre dérivées sont les suivantes :
: <math>\frac{du}{dx}</math>, <math>\frac{dv}{dx}</math>, <math>\frac{du}{dy}</math>, <math>\frac{dv}{dy}</math>
Un bon moyen pour obtenir les dérivées demande de regrouper les pixels par groupes de 4 et de faire la différence entre leurs coordonnées de texture respectives. On peut calculer les deux dérivées horizontales en comparant les deux pixels sur la même ligne, et les deux dérivées verticales en comparant les deux pixels sur la même colonne. Mais cela demande de rastériser les pixels par groupes de 4, par ''quads''. Et c'est ce qui est fait sur les cartes graphiques actuelles, qui rastérisent des groupes de 4 pixels à la fois.
[[File:Texture sampler unit with mipmapping.png|centre|vignette|upright=2.0|Unité de texture avec mipmapping.]]
Malheureusement, le calcul exact utilisé pour le choix de la mip-map dépend du GPU considéré et peu de chose est connu quant à ces algorithmes. Il est possible d'inférer le comportement à partir d'observations, mais guère plus. Pour ceux qui veulent en savoir plus, je conseille la lecture de cet article de blog :
* [https://pema.dev/2025/05/09/mipmaps-too-much-detail/ Mipmap selection in too much detail]
===La gestion des accès mémoire===
Enfin, l'unité de texture doit tenir compte du fait que la mémoire vidéo met du temps à lire une texture. En théorie, l'unité de texture ne devrait pas accepter de nouvelle demande de lecture tant que celle en cours n'est pas terminée. Mais faire ainsi demanderait de bloquer tout le pipeline, de l'''input assembler'' au unités de''shaders'', ce qui est tout sauf pratique et nuirait grandement aux performances.
Une solution alternative consiste à mettre en attente les demandes de lectures de texture pendant que la mémoire est occupée. La manière la plus simple d'implémenter des accès mémoire multiples est de les mettre en attente dans une petite mémoire FIFO. Cela implique que les accès mémoire s’exécutent dans l'ordre demandé par le ''shader'' et/ou l'unité de rastérisation, il n'y a pas de réorganisation des accès mémoire ou d’exécution dans le désordre des accès mémoire.
[[File:Texture prefetching.png|centre|vignette|upright=1.5|Accès mémoire simultanés.]]
Évidemment, quand la mémoire FIFO est pleine, le pipeline est alors totalement bloqué. Le rasteriser est prévenu que l'unité de texture ne peut pas accepter de nouvelle lecture de texture. En pratique, la FIFO est généralement d'une taille respectable et permet de mettre en attente beaucoup de demandes de lecture de texture. Il faut de plus noter qu'il y a une FIFO par processeur de ''shader'' sur les cartes graphiques modernes. Quand elle est pleine, le processeur cesse d'exécuter de nouveaux accès mémoire, mais peut continuer à exécuter des ''shaders'' dans les autres unités de calcul, pas besoin de bloquer complétement le pipeline.
===L'intégration du cache de textures===
Il faut noter que les unités de texture incorporent aussi un cache de texture, voire plusieurs. L'intégration des caches de texture avec la mémoire FIFO précédente est quelque peu compliqué, car il faut garantir que les lectures de texture se fassent dans le bon ordre. On ne peut pas exécuter une lecture dans le cache alors que des lectures précédentes sont en attente de lecture en mémoire vidéo. Et cela pose un gros problème : une lecture dans le cache de texture prend quelques dizaines de cycles d'horloge, alors qu'une lecture en mémoire vidéo en prend facilement 400 à 800 cycles, parfois plus. Et cela fait que l'ordre des accès mémoire peut s'inverser.
Prenons par exemple un accès au cache précédé et suivi par deux accès en mémoire vidéo. Le premier démarre au cycle 1, et se termine au cycle numéro 400. L'accès au cache commence au cycle 2 et se termine 20 cycles après, au cycle numéro 22. En clair, la lecture dans le cache s'est terminée avant l'accès mémoire qui le précède. Les textures ne sont donc plus lues dans l'ordre. Et il faut trouver une solution pour éviter cela.
La solution est de retarder les lectures dans le cache tant que tous les accès précédents ne sont pas terminés. Mais pour retarder les lectures en question, il faut d'abord savoir si la lecture atterrit dans le cache ou non, ce qui demande d'accéder au cache. On fait face à un dilemme : on veut retarder les accès au cache, mais les différencier des lectures déclenchant des accès mémoire demande d'accéder au cache en premier lieu. La solution est décrite dans l'article "Prefetching in a Texture Cache Architecture" par Igehy et ses collègues. Elle se base sur deux idées combinées ensemble.
La première idée est de séparer l'accès au cache en deux : une étape qui vérifie si les texels à lire sont dans le cache, et une étape qui accède aux données dans le cache lui-même. Un cache de texture est donc composé de deux circuits principaux. Le premier vérifie la présence des texels dans le cache. Il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Ensuite, en plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
La seconde idée est de retarder l'accès au cache entre les deux phases. La première étape d'un accès mémoire vérifie si la donnée est dans le cache ou non. Puis, on retarde la lecture des données, pour attendre que toutes les lectures précédentes soient terminées. Et enfin, troisième étape : la lecture des texels dans la mémoire cache proprement dite. Les accès mémoire passant par la mémoire vidéo se font de la même manière, à une différence près : la lecture dans le cache est remplacée par la lecture en mémoire vidéo. Tout démarre avec une demande à l'unité de tags, qui vérifie si le texel est dans le cache ou non. Puis on retarde l'accès tant que la mémoire vidéo est occupée, puis on effectue la lecture en mémoire vidéo.
Si ce n'est pas le cas, l'accès mémoire est envoyé à la mémoire vidéo comme précédemment, à savoir qu'il est mis en attente dans une mémoire FIFO, puis envoyé à la mémoire vidéo dès que celle-ci est libre. Mais en sortie de la mémoire, la donnée lue est envoyée dans le cache de texture, par dans l'unité de filtrage. Pour savoir où placer la donnée lue, l'unité de tag a réservé une ligne de cache précise, une adresse bien précise. L'adresse en question est disponible en lisant une autre mémoire FIFO, qui a mis en attente l'adresse en question, en attendant que l'accès mémoire se termine. La donnée est alors écrite dans le cache, puis lue par l'unité de filtrage de textures.
Pour une lecture dans le cache, le déroulement est similaire, mais sans le passage par la mémoire. La lecture fait une demande à l'unité de tag, et celle-ci répond que la donnée est bien dans le cache. Elle place alors l'adresse à lire dans la file d'attente. Une fois que les accès mémoire précédents sont terminés, l'adresse sort de la file d'attente et est envoyée à la mémoire de données. La lecture s'effectue, les texels sont envoyés à l'unité de filtrage de textures. La seule différence avec un ''phased cache'' normal est l'insertion de l'adresse à lire dans une FIFO qui vise à mettre en attente
[[File:Unité de texture avec un cache de texture.png|centre|vignette|upright=2.0|Unité de texture avec un cache de texture]]
Pour résumer, l'implémentation précédente garantit une exécution des lectures dans leur ordre d'arrivée. Et pour cela, elle retarde les lectures dans le cache tant que les lectures en mémoire précédentes ne sont pas terminées. L'accès au cache est plus rapide que l'accès en mémoire vidéo, mais le retard ajouté pour garantir l'ordre des lectures fait que le temps d'accès est très long.
==Le filtrage de textures==
Plaquer des textures sans autre forme de procès ne suffit pas à garantir des graphismes d'une qualité époustouflante. La raison est que les sommets et les texels ne tombent pas tout pile sur un pixel de l'écran : le sommet associé au texel peut être un petit peu trop en haut, ou trop à gauche, etc. Une explication plus concrète fait intervenir les coordonnées de texture. Souvenez-vous que lorsque l'on traduit une coordonnée de texture u,v en coordonnées x,y, on obtient un résultat qui ne tombe pas forcément juste. Souvent, le résultat a une partie fractionnaire. Si celle-ci est non-nulle, cela signifie que le texel/sommet n'est pas situé exactement sur le pixel voulu et que celui-ci est situé à une certaine distance. Concrètement, le pixel tombe entre quatre texels, comme indiqué ci-dessous.
[[File:Filtrage texture.png|centre|vignette|upright=2.0|Position du pixel par rapport aux texels.]]
Pour résoudre ce problème, on doit utiliser différentes techniques d'interpolation, aussi appelées techniques de '''filtrage de texture''', qui visent à calculer la couleur du pixel final en fonction des texels qui l'entourent. Il existe de nombreux types de filtrage de textures, qu'il s'agisse du filtrage linéaire, bilinéaire, trilinéaire, anisotropique et bien d'autres.
Tous ont besoin d'avoir certaines informations qui sont généralement fournies par les circuits de calcul d'adresse. La première est clairement la partie fractionnaire des coordonnées x,y. La seconde est la dérivée de ces deux coordonnées dans le sens horizontal et vertical., ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Toujours est-il que le filtrage de texture est une opération assez lourde, qui demande beaucoup de calculs arithmétiques. On pourrait en théorie le faire dans les pixels shaders, mais le cout en performance serait absolument insoutenable. Aussi, les cartes graphiques intègrent toutes un circuit dédié au filtrage de texture, le ''texture sampler''. Même les plus anciennes cartes graphiques incorporent une unité de filtrage de texture, ce qui nous montre à quel point cette opération est importante.
[[File:Texture unit.png|centre|vignette|upright=2.0|Unité de texture.]]
On peut configurer la carte graphique de manière à ce qu'elle fasse soit du filtrage bilinéaire, soit du filtrage trilinéaire, on peut configurer le niveau de filtrage anisotropique, etc. Cela peut se faire dans les options de la carte graphique, mais cela peut aussi être géré par l'application. La majorité des jeux vidéos permettent de régler cela dans les options. Ces réglages ne concernent pas la texture elle-même, mais plutôt la manière dont l'unité de texture doit fonctionner. Ces réglages sur l''''état de l'unité de texture''' sont mémorisés quelque part, soit dans l'unité de texture elle-même, soit fournies avec la ressource de texture elle-même, tout dépend de la carte graphique. Certaines cartes graphiques mémorisent ces réglages dans les unités de texture ou dans le processeur de commande, et tout changement demande alors de réinitialiser l'état des unités de texture, ce qui prend un peu de temps. D'autres placent ces réglages dans les ressources de texture elles-mêmes, ce qui rend les modifications de configuration plus rapides, mais demande plus de circuits. D'autres cartes graphiques mélangent les deux options, certains réglages étant globaux, d'autres transmis avec la texture. Bref, difficile de faire des généralités, tout dépend du matériel et le pilote de la carte graphique cache tout cela sous le tapis.
Maintenant que cela est dit, voyons quelles sont les différentes méthodes de filtrage de texture et comment la carte graphique fait pour les calculer.
===Le filtrage au plus proche===
La méthode de filtrage la plus simple consiste à colorier avec le texel le plus proche. Cela revient tout simplement à ne pas tenir compte de la partie fractionnaire des coordonnées x,y, ce qui est très simple à implémenter en matériel. C'est ce que l'on appelle le '''filtrage au plus proche''', aussi appelé ''nearest filtering''.
Autant être franc, le résultat est assez pixelisé et peu agréable à l’œil. Par contre, le résultat est très rapide à calculer, vu qu'il ne demande aucun calcul à proprement parler. Elle ne fait pas appel à la parti fractionnaire des coordonnées entières de texture, ni aux dérivées de ces coordonnées. On peut combiner cette technique avec le mip-mapping, ce qui donne un résultat bien meilleur, bien que loin d'être satisfaisant. Au passage, toutes les techniques de filtrage de texture peuvent se combiner avec du mip-mapping, certaines ne pouvant pas faire sans.
[[File:Interpolation-nearest.svg|centre|vignette|Filtrage de texture au plus proche.]]
===Le filtrage linéaire===
Le filtrage le plus simple est le '''filtrage linéaire'''. Il effectue une interpolation linéaire entre deux mip-maps, deux niveaux de détails. Pour comprendre l'idée, nous allons prendre une situation très simple, avec une texture carrée de 512 texels de côté. Le mip-mapping crée plusieurs textures : une de 256 texels de côté, une de 128 texels, une de 64, etc. Maintenant, la texture est sur un objet à une certaine distance de l'écran, vu de face. Le résultat est qu'elle correspond à l'écran à un carré de 300 pixels de côté (pas d'erreur : pixels, pas texels). Dans ce cas, la texture se trouve entre deux mip-maps : celle de 512 pixels de côté, celle de 256. Laquelle choisir ? Le filtrage au plus proche prend la texture de 512 pixels de côté. Le filtrage linéaire lui, fait autrement.
Vu que la texture est entre deux mip-maps, l'idée est de prendre le texel au plus proche dans chaque texture et de faire une sorte de moyenne appelée l'interpolation linéaire. L'interpolation par du principe que la couleur varie entre les deux texels en suivant une fonction affine, illustrée ci-dessous. Ce ne serait évidemment pas le cas dans le monde réel, mais on supposer cela donne une bonne approximation de ce à quoi ressemblerait une texture à plus haute résolution. On peut alors calculer la couleur du pixel par une simple moyenne pondérée par la distance. Le résultat est que les transitions entre deux niveaux de détails sont plus lisses, moins abruptes.
[[File:Lin interp -é.png|centre|vignette|upright=2.0|Interpolation linéaire.]]
===Le filtrage bilinéaire===
Le filtrage bilinéaire effectue une sorte de moyenne pondérée des quatre texels les plus proches du pixel à afficher. Pour cela, rappelez-vous ce qui a été dit plus haut : les coordonnées x,y d'un pixel ont une partie entière et une partie fractionnaire. Le filtrage au plus proche élimine les parties fractionnaires, ce qui donne une coordonnée x,y. Avec le filtrage bilinéaire, on prend les texels de coordonnées (x,y) ; (x+1,y) ; (x,y+1) ; (x+1,y+1), le pixel étant entre ces 4 texels.
Mais le filtrage ne fait pas qu'une simple moyenne, il prend en compte les parties fractionnaires pour faire la moyenne. En effet, le pixel n'est pas au milieu du carré de texel, il est quelque part mais est souvent plus proche d'un texel que des autres. Et il faut donc pondérer la moyenne par les distances aux 4 texels. Pour cela, la moyenne est calculée à partir d'interpolations linéaires. Avec 4 pixels, nous allons devoir calculer la couleur de deux points intermédiaires. La couleur de ces deux points se calcule par interpolation linéaire, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.
[[File:Bilin3.png|centre|vignette|upright=2|Filtrage bilinéaire de texture.]]
Le circuit qui permet de faire l'interpolation bilinéaire est particulièrement simple. On trouve un circuit de chaque pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu, et un pour la transparence. Chacun de ces circuit est composé de sous-circuits chargés d'effectuer une interpolation linéaire, reliés comme suit.
[[File:Texture sampler unit.png|centre|vignette|Unité de filtrage bilinéaire.]]
Vous noterez que le filtrage bilinéaire accède à 4 pixels en même temps. Fort heureusement, les textures sont stockées de manière à ce qu'on puisse charger les 4 pixels en une fois, comme on l'a vu plus haut. Le filtrage bilinéaire a de fortes chances que les 4 pixels filtrés soient dans la même ''tile'', la seule exception étant quand ils sont tout juste sur le bord d'une ''tile''.
: La console de jeu Nintendo 64 n'utilise que trois pixels au lieu de quatre dans son interpolation bilinéaire, qui en devient une interpolation quasi-bilinéaire. La raison derrière ce choix est une question de performances, comme beaucoup de décisions de ce genre. Le résultat est un rendu imparfait de certaines textures.
===Le filtrage trilinéaire===
Avec le filtrage bilinéaire, des discontinuités apparaissent sur certaines surfaces. Par exemple, pensez à une texture de sol : elle est appliquée plusieurs fois sur toute la surface du sol. A une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512*512 à 256*256, ce qui est visible pour un joueur attentif. De telles transitions sont lissées grâce au filtrage linéaire, il n'y a plus qu'à le combiner avec le filtrage bilinéaire. Rien d’incompatible : le premier filtre l'intérieur d'une mip-map, le second combine deux mip-maps.
Le filtrage trilinéaire prend les deux mip-maps les plus proches, fait un filtrage bilinéaire avec chacune, puis fait une « une moyenne » pondérée entre les deux résultats. Le circuit de filtrage trilinéaire existe en plusieurs versions. La plus simple, illustrée ci-dessous, effectue deux filtrages bilinéaires en parallèle, dans deux circuits séparés, puis combine leurs résultats avec un circuit d'interpolation linéaire. Mais ce circuit nécessite de charger 8 texels simultanément. Qui plus est, ces 8 texels ne sont pas consécutifs en mémoire, car ils sont dans deux niveaux de détails/mip-maps différents.
[[File:Parallel trilinear filtering.png|centre|vignette|upright=2.0|Unité de filtrage trilinéaire parallèle.]]
Vu qu'on lit des texels dans deux mip-maps, les texels sont lus en deux fois : 4 texels provenant de la première mip-map, suivis par les 4 texels de l'autre mip-map. Les 4 premiers texels doivent donc être mis en attente dans des registres, en attendant que les 4 autres arrivent. Une amélioration du circuit précédent gère cela en ajoutant des registres. Il lit les 4 premiers texels, les filtre avec une interpolation bilinéaire, et mémorise le résultat dans un registre. Puis, il lit les 4 autres texels, les filtre, et met le résultat dans un second registre. A ce moment là, un circuit d'interpolation linéaire finit le travail. On économise donc un circuit d'interpolation bilinéaire, sans que les performances soient trop impactées.
[[File:Filtrage trilineaire.png|centre|vignette|upright=1.0|Unité de filtrage trilineaire série.]]
Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le temps où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. À chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients, et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.
===Le filtrage anisotrope===
D'autres artefacts peuvent survenir lors de l'application d'une texture, la perspective pouvant déformer les textures et entraîner l'apparition de flou. La raison à cela est que les techniques de filtrage de texture précédentes partent du principe que la texture est vue de face. Prenez une texture carrée, par exemple. Vue de face, elle ressemble à un carré sur l'écran. Mais tournez la caméra, de manière à voir la texture de biais, avec un angle, et vous verrez que la forme de la texture sur l'écran est un trapèze, pas un carré. Cette déformation liée à la perspective n'est pas prise en compte par les méthodes de filtrage de texture précédentes. Pour le dire autrement, les techniques de filtrage précédentes partent du principe que les 4 texels qui entourent un pixel forment un carré, ce qui est vrai si la texture est vue de face, sans angle, mais ne l'est pas si la texture n'est pas perpendiculaire à l'axe de la caméra. Du point de vue de la caméra, les 4 texels forment un trapèze d'autant moins proche d'un carré que l'angle est grand.
Pour corriger cela, les chercheurs ont inventé le '''filtrage anisotrope'''. En fait, je devrais plutôt dire : LES filtrages anisotropes. Il en existe un grand nombre, dont certains ne sont pas utilisés dans les cartes graphiques actuelles, soit car ils trop gourmand en accès mémoires et en calculs pour être efficaces, soit car ils ne sont pas pratiques à mettre en œuvre. Il est très difficile de savoir quelles sont les techniques de filtrage de texture utilisées par les cartes graphiques, qu'elles soient récentes ou anciennes. Beaucoup de ces technologies sont brevetées ou gardées secrètes, et il faudrait vraiment creuser les brevets déposés par les fabricants de GPU pour en savoir plus. Les algorithmes en question seraient de plus difficiles à comprendre, les méthodes mathématiques cachées derrière ces méthodes de filtrage n'étant pas des plus simple.
[[File:Anisotropic filtering en.png|centre|vignette|upright=2|Exemple de filtrage anisotrope.]]
==La compression de textures==
Les textures les plus grosses peuvent aller jusqu'au mébioctet, ce qui est beaucoup. Pour limiter la casse, les textures sont compressées. La '''compression de texture''' réduit la taille des textures, ce qui peut se faire avec ou sans perte de qualité. Elle entraîne souvent une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande. Mais il s'agit là d'une technique très simple, beaucoup plus simple que les techniques que nous allons voir dans cette section. Nous allons voir quelque algorithmes de compression de textures de complexité intermédiaire, mais n'allons pas voir l'état de l'art. Il existe des formats de texture plus récents que ceux qui nous allons aborder, comme l{{'}}''Ericsson Texture Compression'' ou l{{'}}''Adaptive Scalable Texture Compression'', plus complexes et plus efficaces.
Notons que les textures sont compressées dans les fichiers du jeu, mais aussi en mémoire vidéo. Les textures sont décompressées lors de la lecture. Pour cela, la carte graphique contient alors un circuit, capable de décompresser les textures lorsqu'on les lit en mémoire vidéo. Les cartes graphiques supportent un grand nombre de formats de textures, au niveau du circuit de décompression. Du fait que les textures sont décompressées à la volée, les techniques de compression utilisées sont assez particulières. La carte graphique ne peut pas décompresser une texture entière avant de pouvoir l'utiliser dans un ''pixel shader''. A la place, on doit pouvoir lire un morceau de texture, et le décompresser à la volée. On ne peut utiliser les méthodes de compression du JPEG, ou d'autres formats de compression d'image. Ces dernières ne permettent pas de décompresser une image morceau par morceau.
Pour permettre une décompression/compression à la volée, les textures sont des textures tilées, généralement découpées en tiles de 4 * 4 texels. Les ''tiles'' sont compressées indépendamment les unes des autres. Et surtout, avec ou sans compression, la position des tiles en mémoire ne change pas. On trouve toujours une tile tous les T octets, peu importe que la tile soit compressée ou non. Par contre, une tile compressée n'occupera pas T octets, mais moins, là où une tile compressée occupera la totalité des T octets. En clair, compresser une tile fait qu'il y a des vides entre deux tiles dans al mémoire vidéo, mais ne change rien à leur place en mémoire vidéo qui est prédéterminée, peu importe que la texture soit compressée ou non. L'intérêt de la compression de textures n'est pas de réduire la taille de la texture en mémoire vidéo, mais de réduire la quantité de données à lire/écrire en mémoire vidéo. Au lieu de lire T octets pour une tile non-compressée, on pourra en lire moins.
===La palette indicée et la technique de ''Vector quantization''===
La technique de compression des textures la plus simple est celle de la '''palette indicée''', que l'on a entraperçue dans le chapitre sur les cartes d'affichage. La technique de '''''vector quantization''''' peut être vue comme une amélioration de la palette, qui travaille non pas sur des texels, mais sur des ''tiles''. À l'intérieur de la carte graphique, on trouve une table qui stocke toutes les ''tiles'' possibles. Chaque ''tile'' se voit attribuer un numéro, et la texture sera composé d'une suite de ces numéros. Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l’embarqué utilisent ce genre de compression.
===Les algorithmes de ''Block Truncation coding''===
La première technique de compression élaborée est celle du '''''Block Truncation Coding''''', qui ne marche que pour les images en niveaux de gris. Le BTC ne mémorise que deux niveaux de gris par ''tile'', que nous appellerons couleur 1 et couleur 2, les deux niveaux de gris n'étant pas le même d'une ''tile'' à l'autre. Chaque pixel d'une ''tile'' est obligatoirement colorié avec un de ces niveaux de gris. Pour chaque pixel d'une ''tile'', on mémorise sa couleur avec un bit : 0 pour couleur 1, et 1 pour couleur 2. Chaque ''tile'' est donc codée par deux entiers, qui codent chacun un niveau de gris, et une suite de bits pour les pixels proprement dit. Le circuit de décompression est alors vraiment très simple, comme illustré ci-dessous.
[[File:Block Truncation coding.jpg|centre|vignette|upright=2.0|Block Truncation coding.]]
La technique du BTC peut être appliquée non pas du des niveaux de gris, mais pour chaque composante Rouge, Vert et Bleu. Dans ces conditions, chaque ''tile'' est séparée en trois sous-''tiles'' : un sous-bloc pour la composante verte, un autre pour le rouge, et un dernier pour le bleu. Cela prend donc trois fois plus de place en mémoire que le BTC pur, mais cela permet de gérer les images couleur.
===Le format de compression S3TC / DXTC===
L'algorithme de '''Color Cell Compression''', ou CCC, améliore le BTC pour qu'il gère des couleurs autre que des niveaux de gris. Ce CCC remplace les deux niveaux de gris par deux couleurs. Une ''tile'' est donc codée avec un entier 32 bits par couleur, et une suite de bits pour les pixels. Le circuit de décompression est identique à celui utilisé pour le BTC.
[[File:Color Cell Compression.jpg|centre|vignette|Color Cell Compression.]]
[[File:Dxt1-memory-layout.png|vignette|Dxt1 et ''color cell compression''.]]
Le format de compression de texture utilisé de base par Direct X, le DXTC, est une version amliorée de l'algorithme précédent. Il est décliné en plusieurs versions : DXTC1, DXTC2, etc. La première version du DXTC est une sorte d'amélioration du CCC : il ajoute une gestion minimale de transparence, et découpe la texture à compresser en ''tiles'' de 4 pixels de côté. La différence, c'est que la couleur finale d'un texel est un mélange des deux couleurs attribuée au bloc. Pour indiquer comment faire ce mélange, on trouve deux bits de contrôle par texel.
Si jamais la couleur 1 < couleur2, ces deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (2 * Couleur1 + Couleur2) / 3
* 11 = (Couleur1 + 2 * Couleur2) / 3
Sinon, les deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (Couleur1 + Couleur2) / 2
* 11 = Transparent
[[File:DXTC.jpg|centre|vignette|DXTC.]]
Le circuit de décompression du DXTC ressemble alors à ceci :
[[File:Circuit de décompression du DXTC.jpg|centre|vignette|upright=2.0|Circuit de décompression du DXTC.]]
===Les format DXTC 2, 3, 4 et 5 : l'ajout de la transparence===
Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3, lui-même replacé par le DXT4 et par le DXT5. Dans le DXT3, la transparence fait son apparition. Pour cela, on ajoute 64 bits par ''tile'' pour stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1.
[[File:Dxt23-memory-layout.png|centre|vignette|Dxt 2 et 3.]]
Dans le DXT4 et le DXT5, la méthode utilisée pour compresser les couleurs l'est aussi pour les valeurs de transparence. L'information de transparence est stockée par un en-tête contenant deux valeurs de transparence, le tout suivi d'une matrice qui attribue trois bits à chaque texel. En fonction de la valeur des trois bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1.
[[File:Dxt45-memory-layout.png|centre|vignette|Dxt 4 et 5.]]
===Le format de compression PVRTC===
Passons maintenant à un format de compression de texture un peu moins connu, mais pourtant omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle travaille surtout dans les cartes graphiques embarquées. Ses cartes se trouvent notamment dans l'ipad, l'iPhone, et bien d'autres smartphones actuels.
Avec le PVRTC, les textures sont encore une fois découpées en ''tiles'' de 4 texels par 4, mais la ressemblance avec le DXTC s’arrête là. Chacque ''tile'' est codée avec :
* une couleur codée sur 16 bits ;
* une couleur codée sur 15 bits ;
* 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
* et un bit de modulation, qui permet de configurer l’interprétation des bits de mélange.
Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4*4.
==Annexe : le ''normal-mapping'' hardware==
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, parlons un peu du ''normal mapping'' et de son implémentation dans les unités de texture. Pour rappel, 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. Et pour comprendre quel est le rapport avec les textures, nous allons devoir faire quelques rappels sur l'éclairage par pixel.
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
L'éclairage se fait en utilisant de nombreux calculs, qu'on a détaillé dans le chapitre sur les bases du rendu 3D. Ceux-ci utilisent la normale d'un sommet, à savoir un vecteur orienté à la verticale de la surface. En théorie, il y a une normale par sommet, ce qui fait que les calculs d'éclairage doivent se faire au niveau géométrique, avant la rastérisation. Mais pour obtenir un éclairage de meilleure qualité, il y a des techniques qui calcule l'éclairage d'une scène 3D pixel par pixel.
Une première technique d'éclairage par pixel interpole les normales lors de l'étape de rastérisation. On obtient alors un éclairage de Phong. Une autre solution est celle du ''normal mapping''. Le '''''normal mapping''''' précalcule les normales d'une surface dans une texture, appelée la ''normal map''. L'éclairage est alors réalisé par un pixel shader, qui lit les normales depuis cette texture et fait les calculs d'éclairage avec.
===L'usage de textures non-compressées pour les ''normal maps''===
La ''normal map'' est une texture, donc. Mais ce n'est pas une texture comme une autre. Elle mémorise un vecteur pour chaque texel, pas une couleur. Il est possible d'utiliser les formats de textures non-compressés, même si cela ne donne pas de bons résultats. Après tout, un vecteur est codé par trois coordonnées x,y,z, il est possible de coder chacune avec un octet et de packager cela dans une texture RGB classique. La coordonnée x va dans la composante Rouge, la coordonnée Y dans la composante Bleu, et la coordonnée z dans la composante z.
Une première optimisation est de ne pas mémoriser les trois coordonnées, mais seulement deux d'entre elles et de calculer la troisième. En effet, les normales sont des vecteurs ''normalisés'', c'est à dire que leur longueur vaut 1, par construction. Vu que la taille est connue à l'avance, on peut en déduire la coordonnée z à partir des coordonnées x et y, avec l'usage du théorème de Pythagore. Par définition, <math>z^2 = 1 - x^2 - y^2</math>. Le calcul peut être fait dans le ''pixel shader'' sans problème.
: Pour être précis, il faut que les normales soient définies d'une certaine manière pour que ça marche. Les normales ne sont pas définies dans le même système de coordonnées que le modèle 3D, mais dans un autre système appelé l'''espace tangent''.
Notons que l'accès à la ''normal map'' se fait comme pour n'importe quelle texture : les texels sont lus en mémoire vidéo, puis le filtrage de texture est appliqué, et enfin le tout est envoyé au ''pixel shader''. En théorie, le filtrage effectue une sorte d'interpolation des normales automatique, ce qui permet de trouver la normale à un pixel précis, même s'il n'est pas sur un texel.
Les solutions précédentes demandent d'utiliser des textures non-compressées, qui utilisent beaucoup de mémoire vidéo. Utiliser des textures compressées pourrait sembler résoudre le problème. Mais l'usage de textures compressées marche très mal pour les ''normal maps'', elles ne permettent pas d'obtenir une qualité ou une compression suffisante. Aussi, des formats de texture dédiés aux ''normal maps'' ont été inventés.
===Le format de texture 3Dc===
Le problème est que le rendu final n'est pas très beau. Et les opérations de filtrage ne donnent pas de très bons résultats. Pour remédier à ces problèmes, des formats de texture spécialisés pour les ''normal map'' ont été inventés. Le premier d'entre eux est le '''3Dc''', aussi appelé BC5 pour ''Block Compression 5''. Il utilise un octet par texel, au lieu de trois avec une texture RGB normale non-compressée. Et surtout : il est géré en matériel, directement dans l'unité de texture, qui peut décompresser les textures 3Dc et les filtrer !
Avec le 3Dc, seules coordonnées sont mémorisées, la troisième est calculée, comme dit plus haut. Pour le reste, la compression de la ''normal map'' ressemble un peu à celle des textures. La ''normal map'' est découpée en blocs de 4 par 4 texels de côté, chacune étant encodée en tenant compte de certaines redondances. Dans un bloc, les coordonnées x sont dans un certain intervalle, allant de <math>x_{min}</math> à <math>x_{max}</math>, idem pour la coordonnée y qui est dans l'intervalle <math>y_{min}</math>, <math>y_{max}</math>. L'intervalle <math>x_{min}</math> à <math>x_{max}</math> est coupé en 8 parts égales, ce qui fait 8 valeurs possibles dans cet intervalle, chacune correspondant à une valeur x possible pour une normale du bloc. La normale est donc encodée en précisant quelle valeur est la bonne, en utilisant un indice de 3 bits pour cela. La valeur exacte de la coordonnée x se calcule à partir de l'indice comme suit :
: <math>x = x_{min} + \left( \frac{x_{min} - x_{min}}{8} \right) \times \text{indice} </math>
Pour encoder un bloc de 4 par 4 normales, il faut donc :
* Un octet pour chaque coordonnée <math>x_{min}</math>, <math>x_{max}</math>, <math>y_{min}</math>, <math>y_{max}</math>.
* 6 bits par normale : 3 pour encoder le décalage de la coordonnée x, 3 pour la coordonnée y.
Le tout permet d'encoder un bloc de 16 normales en seulement 128 bits, soit un octet par normale. Le ''pixel shader'' se débrouille pour décompresser un bloc. Il effectue notamment le calcul du dessus, pour retrouver la coordonnée x. Les calculs étant particulièrement simples, le cout en performance est très faible. Et ce d'autant plus qu'ils demandent juste de faire des additions et des multiplications entières, que l'unité scalaire peut faire en parallèle d'autres opérations plus gourmandes. Et le cout en calculs est de toute façon compensé par l'économie de mémoire et de bande passante lié à la compression : diviser la taille des données par trois, ca a un sacré impact !
==Annexe : les ''shadowmap'' hardware==
Les anciens GPU, notamment la Geforce FX, avaient des fonctionnalités spécifiques pour le calcul des ombres. Dans la plupart des jeux vidéos de l'époque, et même de maintenant, les ombres sont calculées avec la technique des ''shadowmap''. L'idée est assez simple sur le principe : un pixel est dans l'ombre quand il est invisible depuis une source de lumière.
L'idée est que le rendu est réalisé en plusieurs passes, avec une passe par source de lumière et une passe finale pour calculer l'image finale. Nous allons expliquer la technique avec une seule source de lumière, et allons utiliser l'exemple de la scène ci-dessous.
[[File:7fin.png|centre|vignette|Scène 3D d'exemple.]]
===La technique du ''shadowmapping''===
[[File:2shadowmap.png|vignette|Résultat de la première passe : ''shadowmap''..]]
La première passe rend l'image depuis le point de vue de la source de lumière. Cette première passe ne rend pas les couleurs de la scène, elle ne s'intéresse qu'à la profondeur des pixels. Le résultat est que l'image ne rend que le tampon de profondeur. Celui-ci est ensuite réutilisé comme texture pour la passe suivante. La texture en question est appelée la '''''shadownmap'''''.
La perspective utilisée, ainsi que le ''view frustrum'', dépend de la source de lumière. Pour une source de lumière qui émet un cône de lumière, le ''view frustrum'' de l'image rendue doit contenir tout le cone de lumière, et doit coller le plus possible à celui-ci. Pour une source directionnelle, comme le soleil, une perspective orthographique est utilisée.
La seconde passe rend l'image du point de vue de la caméra, pour rendre l'image finale. Elle rend l'image finale, qui est composée de pixels, chacun ayant une position à l'écran x,y, et une profondeur z. Les coordonnées sont transformées pour obtenir la position de ce pixel depuis le point de vue de la caméra. Une simple multiplication de matrice suffit, rien de bien compliqué, un shader peut le faire.
[[File:5failed.png|vignette|Résultat du test des comparaisons.]]
Après cette étape, on a alors les coordonnées x,y,z de ce pixel du point de vue de la caméra, et la ''shadowmap''. Il est alors possible d'accéder à la ''shadowmap'' au même endroit, à la même place que le pixel testé, aux mêmes coordonnées x,y. Si la profondeur du pixel est supérieure à celle de la shadowmap au même endroit, alors le pixel est situé derrière la surface visible, donc est dans l'ombre. Sinon, il n'est pas dans l'ombre. Le même procédé est répété sur chaque pixel de l'écran.
===Les optimisations hardware du ''shadowmapping''===
La technique des ''shadowmap'' demande donc de calculer une texture ''shadowmap'', puis de lire celle-ci et de faire des comparaisons de profondeur. Les GPU comme la Geforce FX intégraient du matériel dans les unités de texture pour faciliter ce travail. Les unités de texture pouvaient lire les ''shadowmap'', et faire la comparaison de profondeur toutes seules, elles avaient des circuits pour. Il suffisait de leur fournir le pixel à tester, ses coordonnées x,y,z, et l'adresse de la ''shadowmap''. Les unités de texture renvoyaient alors un résultat valant 0 ou 1 : 1 si le pixel est dans l'ombre, 0 sinon.
Elles pouvaient même effectuer du filtrage de texture sur les ''shadowmap''. Mais le filtrage était différent de celui utilisé sur les autres textures : moyenner des valeurs de profondeur ne marche pas bien. Elles utilisaient des techniques de filtrage différentes : elles faisaient les tests de comparaison, puis faisaient la moyenne des résultats. Ainsi, pour du filtrage bilinéaire, elles lisaient 4 texels dans la ''shadowmap'', puis faisaient 4 tests de comparaison, et moyennaient les 4 résultats.
==Annexe : les textures virtuelles==
Les '''textures virtuelles''' sont une optimisation des textures normales, qui visent à accélérer le rendu de terrains de grande taille. Imaginez par exemple un monde assez ouvert, comme un environnement en forêt ou en montagne, avec une grande distance de visibilité. Avec de tels terrains, le "sol" est recouvert par une texture de sol unique qui recouvre tout le terrain. Elle ne se répète pas, est de très grande taille, et peut parfois recouvrir toute la map ! Mais il n'y a pas assez de mémoire vidéo pour mémoriser la texture toute entière. La seule solution est la suivante : une partie de la texture est placée en mémoire vidéo, le reste est soit placé en mémoire RAM ou sur le disque dur.
Pour cela, le moteur de jeu utilise une optimisation ingénieuse, basée sur une observation assez basique : une bonne partie de la texture est visible, mais le reste est caché par des arbres, des habitations ou d'autres obstacles. Une optimisation possible de ne garder en mémoire vidéo que les portions visibles de la texture, pas les portions cachées. Une autre optimisation mélange textures virtuelles et ''mip-mapping''. L'idée est que pour les portions lointaines d'une texture, la texture utilisée est une ''mip-map'' de basse résolution. L'idée est alors de ne charger que la ''mip-map'' adéquate, pas les autres niveaux de détail. En clair, la texture de base n'est pas chargée en mémoire vidéo, mais la ''mip-map'' basse résolution l'est.
===Une texture à deux niveaux===
L'implémentation des textures virtuelles découpe les méga-textures en ''tiles'', en morceaux rectangulaires de taille modeste. En clair, le terrain est découpé en morceau rectangulaires/carrés. Seules les tiles nécessaires sont chargées en mémoire vidéo, pas les autres. Par exemple, les ''tiles'' non-visibles ne sont pas placées en mémoire vidéo, seules les ''tiles'' visibles le sont. De même, il y a une ''tile'' par niveau de mip-map : seul la tile correspondant le niveau adéquat est en mémoire vidéo, les autres niveaux de détail ne sont pas chargés. On peut faire une analogie avec la mémoire virtuelle, où les données sont découpées en pages, qui sont chargées en mémoire RAM à la demande, suivant les besoins, les données pouvant être swappées sur le disque dur si elles sont peu utilisées. Sauf qu'ici, il s'agit de textures qui sont découpées en pages chargées à la demande en mémoire vidéo, depuis la RAM système.
Une texture virtuelle est en réalité un système à deux niveaux : une liste de ''tiles'' et les ''tiles'' elles-mêmes. La liste de ''tiles'' est appelée un '''atlas de texture''', c'est un peu l'équivalent de la ''tilemap'' pour le rendu 2D. Rendre une texture demande de calculer quelle ''tile'' contient le texel à afficher, consulter la ''tile'' en question, puis récupérer le texel adéquat dans cette ''tile''. La ''tile'' est donc une texture, mais la texture à charger est choisie parmi un ensemble, qui est ici l'atlas de texture.
===L'implémentation : logicielle versus matérielle===
Les textures virtuelles ont été utilisées pour la première fois par les jeux Rage 1 et 2 d'IdSoftware, et quelques jeux ultérieurs comme DOOM 2016. IdSoftware les appelait des '''''mega-textures'''''. L'optimisation permettait des gains en performance assez impressionnants. Le jeu Rage 1 utilisait une texture carrée unique de 128k pixels de côté pour rendre le terrain. En théorie, une telle texture devrait prendre 64 giga-octets, mais le jeu tournait correctement avec 512 méga-octets de RAM, poussivement avec seulement 256 méga-octets de RAM.
De nos jours, les textures virtuelles sont supportées par beaucoup de jeux vidéos, les moteurs les plus courants gèrent de telles textures de manière logicielles. Mais quelques GPU récents supportent les textures virtuelles. Sur les GPU récents, l'atlas de texture est géré nativement par le matériel. Le GPU choisit quelle ''tile'', quelle texture choisir pour rendre le texel adéquat. Pour cela, le GPU calcule quelle ''tile'' charger, consulte l'atlas de texture, et lit la texture de ''tile'' adéquate.
Mais l'implémentation sur les GPU récents a de nombreuses limitations. La limitation la plus importante est que la taille des textures virtuelles ne peut pas dépasser la taille d'une texture normale, soit 32768 pixels de côté pour une texture carrée environ sur les GPU de 2020. De plus, le chargement d'une ''tile'' est très lent. En clair, dès qu'on veut changer de niveau de mip-map pour une tile, ou dès qu'une tile devient visible, le chargement de la tile peut facilement prendre plusieurs centaines de millisecondes. Le filtrage de texture est très complexe avec des textures virtuelles, ce qui fait que le filtrage de texture virtuelle est souvent soumis à des limitations que les textures normales n'ont pas, notamment pour le filtrage anisotropique.
{{NavChapitre | book=Les cartes graphiques
| prev=Le rasterizeur
| prevText=Le rasterizeur
| next=Les Render Output Target
| nextText=Les Render Output Target
}}{{autocat}}
r0fx2s6mx0ps46updr0eu3j8n7t50dn
763419
763418
2026-04-10T19:25:57Z
Mewtow
31375
763419
wikitext
text/x-wiki
[[File:Texture mapping.png|vignette|''Texture mapping'']]
Les '''textures''' sont des images que l'on va plaquer sur la surface d'un objet, du papier peint en quelque sorte. Les cartes graphiques supportent divers formats de textures, qui indiquent comment les pixels de l'image sont stockés en mémoire : RGB, RGBA, niveaux de gris, etc. Une texture est donc composée de "pixels", comme toute image numérique. Pour bien faire la différence entre les pixels d'une texture, et les pixels de l'écran, les pixels d'une texture sont couramment appelés des ''texels''.
==Le placage de textures inverse==
Pour rappel, plaquer une texture sur un objet consiste à attribuer un texel à chaque sommet, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture.
Dans les faits, on n'utilise pas de coordonnées entières de ce type. Les coordonnées de texture sont deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. Le nom donnée à cette technique de description des coordonnées de texture s'appelle l''''''UV Mapping'''''.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Les API 3D modernes gèrent des textures en trois dimensions, ce qui ajoute une troisième coordonnée de texture notée w. Dans ce qui va suivre, nous allons passer les textures en trois dimensions sous silence. Elles ne sont pas très utilisées, la quasi-totalité des jeux vidéo et applications 3D utilisant des textures en deux dimensions. Par contre, le matériel doit gérer les textures 3D, ce qui le rend plus complexe que prévu. Il faut ajouter quelques circuits pour, de quoi gérer la troisième coordonnée de texture, etc.
Lors de la rastérisation, chaque fragment se voit attribuer un sommet, et donc la coordonnée de texture qui va avec. Si un pixel est situé pile sur un sommet, la coordonnée de texture de ce sommet est attribuée au pixel. Si ce n'est pas le cas, la coordonnée de texture finale est interpolée à partir des coordonnées des trois sommets du triangle rastérisé. L'interpolation en question a lieu dans l'étape de rastérisation, comme nous l'avons vu dans le chapitre précédent. Le fait qu'il y ait une interpolation fait que les coordonnées du pixel gagent à être des nombres flottants. On pourrait faire une interpolation avec des coordonnées de texture entières, mais les arrondis et autres imprécisions de calcul donneraient un résultat graphiquement pas terrible, et empêcheraient d'utiliser les techniques de filtrage de texture que nous verrons dans ce chapitre.
À partir de ces coordonnées de texture, la carte graphique calcule l'adresse du texel qui correspond, et se charge de le lire. Et toute la magie a lieu dans ce calcul d'adresse, qui part de coordonnées de texture flottante, pour arriver à une adresse mémoire. Le calcul de l'adresse du texel se fait en plusieurs étapes, que nous allons voir ci-dessous. La première étape convertit les coordonnées flottantes en coordonnées entières, qui disent à quel ligne et colonne se trouve le texel voulu dans la texture. L'étape suivante transforme ces coordonnées x,y entières en adresse mémoire.
===La normalisation des coordonnées===
J'ai dit plus haut que les coordonnées de texture sont des coordonnées flottantes, comprises entre 0 et 1. Mais il faut savoir que les pixels shaders peuvent modifier celles-ci pour mettre en œuvre certains effets graphiques. Et le résultat peut alors se retrouver en-dehors de l'intervalle 0,1. C'est quelque chose de voulu et qui est traité par la carte graphique automatiquement, sans que ce soit une erreur. Au contraire, la manière dont la carte graphique traite cette situation permet d'implémenter des effets graphiques comme des textures en damier ou en miroir.
[[File:Clamp tile.jpg|vignette|Clamp tile]]
Il existe globalement trois méthodes très simples pour gérer cette situation, qui sont appelés des '''modes d'adressage de texture'''.
* La première méthode est de faire en sorte que le résultat sature. Si une coordonnée est inférieur à 0, alors on la remplace par un zéro. Si elle est supérieure à 1, on la ramène à 1. Avec cette méthode, tout se passe comme si les bords de la texture étaient étendus et remplissaient tout l'espace autour de la texture. Le tout est illustré ci-dessous. Ce mode d'accès aux textures est appelé le '''''clamp'''''.
* Une autre solution retire la partie entière de la coordonnée, elle coupe tout ce qui dépasse 1. Pour le dire autrement, elle calcule le résultat modulo 1 de la coordonnée. Le résultat est que tout se passe comme si la texture était répétée à l'infini et qu'elle pavait le plan.
* Une autre méthode remplit les coordonnées qui sortent de l’intervalle 0,1 avec une couleur préétablie, configurée par le programmeur.
===La conversion des coordonnées de textures flottantes en adresse mémoire===
Une fois la normalisation effectuée, les coordonnées de texture sont utilisées pour lire le texel voulu. Pour cela, les coordonnées de texte sont transformées en adresse mémoire, adresse qui pointe sur le texel ayant ces cordonnées. Pour cela, la première étape est de transformer les coordonnées flottantes u,v en coordonnées entières x,y qui pointent sur un texel. Pour cela, il suffit de multiplier les coordonnées flottantes u,v par la résolution de la texture accédée. Pour un écran de résolution <math>\text{height,width}</math>, le calcul est le suivant :
: <math>x = u \times \text{width}</math>
: <math>y = v \times \text{height}</math>
Le résultat est un nombre avec une partie entière et une partie fractionnaire. La partie entière des deux coordonnées donne la position x,y voulue, et la partie fractionnaire est conservée pour le filtrage de textures, mais passons cela sous silence pour le moment.
La seconde étape prend les coordonnées entières x,y et calcule l'adresse mémoire du texel. L'adresse dépend de la position de la texture en mémoire, précisément de son début, son premier texel, mais aussi de la position du texel par rapport au début de la texture. Et calculer cette position intra-texture dépend de la manière dont les texels sont stockés en mémoire.
====Les textures naïves====
Les programmeurs qui lisent ce cours s'attendent certainement à ce que la texture soit stockée en mémoire ligne par ligne, ou colonne par colonne. Cela veut dire que le premier pixel en partant d'en haut à gauche est stocké en premier, puis celui immédiatement à sa droite, puis celui encore à droite, et ainsi de suite. Une fois qu'on arrive à la fin d'une ligne, on passe à la ligne suivante, en-dessous. Cette organisation ligne par ligne s'appele l'organisation '''''row major order'''''. On peut faire pareil, mais colonne par colonne, ce qui donne le '''''column major order'''''.
[[File:Speicheranordnung Feld.svg|centre|vignette|upright=2|Row et column major order.]]
Maintenant, supposons que la texture commence à l'adresse <math>A_\text{texture}</math>, qui est l'adresse du premier texel. La texture a une résolution de <math>\text{width}</math> texels de large et <math>\text{height}</math> texels de haut. Par définition, les coordonnées X et Y des texels commencent à 0, ce qui fait que le pixel en haut à gauche a les coordonnées 0,0.
L'adresse du pixel se calcule comme suit :
: <math>A_\text{pixel} = A_\text{texture} + (\text{taille d'une ligne en octets} \times Y) + (\text{taille d'un texel en octets} \times X)</math>
La taille d'un pixel en mémoire est notée T. La taille d'une ligne en mémoire est de <math>width \times T</math>, par définition, vu qu'elle fait <math>width</math> texels. On a donc :
: <math>A_\text{pixel} = A_\text{texture} + (width \times T \times Y) + (T \times X)</math>
La formule se réécrit comme suit :
: <math>A_\text{pixel} = A_\text{texture} + T \times (width \times Y + X)</math>
Le calcul d'adresse est donc assez simple. Malheureusement, les textures ne sont pas stockées de cette manière en mémoire vidéo. En effet, elle se marie mal avec les opérations de filtrage de texture que nous allons voir dans ce qui suit. Le filtrage d'un texel dépend de ses voisins du dessus et du dessous. Le fait que la texture n'est pas forcément parcourue ligne par ligne fait que stocker une texture ligne par ligne n'est pas l'idéal.
De même, les textures sont déformées par la perspective. L'affichage de la texture ne se fait alors pas ligne par ligne, mais en parcourant la texture en diagonale, l'angle de la diagonale correspondant approximativement à l'angle que fait la verticale de la texture avec le regard. Vu qu'on ne connait pas à l'avance l'angle que fera la diagonale de parcours, on doit ruser.
====Les textures tilées====
Une première solution à ce problème est celle des '''textures tilées'''. Avec ces textures, l'image de la texture est découpée en ''tiles'', des rectangles ou en carrés de taille fixe, généralement des carrés de 4 pixels de côté. Les tiles ont une largeur et une longueur égales, afin de simplifier les calculs : on divise X et Y par le même nombre. De plus, leur largeur et leur longueur sont une puissance de deux, afin de simplifier les calculs d'adresse. Les ''tiles'' sont alors mémorisée les unes après les autres dans le fichier de la texture.
[[File:Texture tilée.png|centre|vignette|upright=2|Texture tilée]]
La formule de calcul d'adresse vue plus haut doit être adaptée pour tenir compte des tiles. Pour cela, il faut remplacer la taille d'un texel par la taille d'une tile, et que la largeur de la texture soit exprimée en nombre de tiles. De plus, on doit adapter les coordonnées des texels pour donner des coordonnées de tile. Généralement, les tiles sont des carrés de N pixels de côté, ce qui fait qu'on peut regrouper les lignes et les colonnes par paquets de N. Il suffit donc de diviser Y et X pour obtenir les coordonnées de la tile, de même que la larguer. La formule pour calculer la position de la énième tile est alors la suivante :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + \text{Taille mémoire d'une tile} \times \left( {\text{Width} \over N} \times {Y \over N} + {X \over N} \right)</math>
On peut réécrire le tout comme suit :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + K \times \left( {Y \over N} + X \right)</math>, avec K une constante connue à la compilation des shaders.
Vu que les tiles sont carrées avec une largeur qui est une puissance de deux, la multiplication par la taille d'une tile en mémoire se simplifie : on passe d'une multiplication entière à des décalages de bits. Même chose pour le calcul de l'adresse de la tile à partir des coordonnées x,y : ils impliquent des divisions par une puissance de deux, qui deviennent de simples décalages.
La position d'un pixel dans une tile dépend du format de la texture, mais peut se calculer avec quelques calculs arithmétiques simples. Dans les cas les plus simples, les pixels sont mémorisés ligne par ligne, ou colonne par colonne. Mais ce n'est pas systématiquement le cas. Toujours est-il que les calculs pour déterminer l'adresse sont simples, et ne demandent que quelques additions ou multiplications. Mais avec les formats de texture utilisés actuellement, les tiles sont chargées en entier dans le cache de texture, sans compter que diverses techniques de compression viennent mettre le bazar, comme on le verra dans la suite de cours.
Un avantage de l'organisation en tiles est qu'elle se marie bien avec le parcours des textures. On peut parcourir une texture dans tous les sens, horizontal, vertical, ou diagonal, on sait que les prochains pixels ont de fortes chances d'être dans la même tile. Si on rentre dans une tile par la gauche en haut, on a encore quelques pixels à parcourir dans la tile, par exemple. De même, le filtrage de textures est facilité. On verra dans ce qui va suivre que le filtrage de texture a besoin de lire des blocs de 4 texels, des carrés de 2 pixels de côté. Avec l'organisation en tile, on est certain que les 4 texels seront dans la même tile, sauf s'ils ont le malheur d'être tout au bord d'une tile. Ce dernier cas est assez rare, et il l'est d'autant plus que les tiles sont grandes. Enfin, un dernier avantage est que les tiles sont généralement assez petites pour tenir tout entier dans une ligne de cache. Le cache de texture est donc utilisé à merveille, ce qui rend les accès aux textures plus rapides.
====Les textures basées sur des ''z-order curves''====
Les formats de textures théoriquement optimaux utilisent une '''''Z-order curve''''', illustrée ci-dessous. L'idée est de découper la texture en quatre rectangles identiques, et de stocker ceux-ci les uns à la suite des autres. L'intérieur de ces rectangles est lui aussi découpé en quatre rectangles, et ainsi de suite. Au final, l'ordre des pixels en mémoire est celui illustré ci-dessous.
[[File:Z-CURVE.svg|centre|vignette|upright=2|Construction d'une ''Z-order curve''.]]
Les texels sont stockés les uns à la suite des autres dans la mémoire, en suivant l'ordre donnée par la ''Z-order curve''. Le calcul d'adresse calcule la position du texel en mémoire, par rapport au début de la texture, et ajoute l'adresse du début de la texture. Mais tout le défi est de calculer la position d'un texel en mémoire, à partir des coordonnées x,y. Le calcul peut sembler très compliqué, mais il n'en est rien. Le calcul demande juste de regarder les bits des deux coordonnées et de les combiner d'une manière particulièrement simple. Il suffit de placer le bit de poids fort de la coordonnée x, suivi de celui de la coordonnée y, et de faire ainsi de suite en passant aux bits suivants.
[[File:Zcurve45bits.png|centre|vignette|upright=1.5|Calcul de la position d'un élément dans une ''Z-order curve'' à partir des coordonnées x et y.]]
L'avantage d'une telle organisation est que la textures est découpées en ''tiles'' rectangulaires d'une certaine taille, elles-mêmes découpées en ''tiles'' plus petites, etc. Et il se trouve que cette organisation est parfaite pour le cache de texture. L'idéal pour le cache de texture est de charger une ''tile'' complète dans le cache de textures. Quand on accède à un texel, on s'assure que la ''tile'' complète soit chargée. Mais cela demande de connaitre à l'avance la taille d'une ''tile''. Les formats de texture fournissent généralement une ''tile'' carré de 4 pixels de côté, mais cela donnerait un cache trop petit pour être vraiment utile. Avec cette méthode, on s'assure qu'il y ait une ''tile'' avec la taille optimale. Les ''tiles'' étant découpées en ''tiles'' plus petites, elles-mêmes découpées, et ainsi de suite, on s'assure que la texture est découpées en ''tiles'' de taille variées. Il y aura au moins une ''tile'' qui rentrera tout pile dans le cache.
===Les techniques de rendu à textures multiples===
Nous venons de voir comment une texture est plaquée sur un objet 3D, ou une surface comme un sol. Pour résumer, le calcul de l'adresse d'un texel prend la position du texel par rapport au début de la texture, et ajoute l'adresse du début de la texture. L'adresse mémoire de la texture est connue au moment où le pilote de la carte graphique place la texture dans la mémoire vidéo, et cette information est transmise au matériel par l'intermédiaire du processeur de commande, puis passée aux processeurs de shaders et à l'unité de texture. Le tout est couplé à d'autres informations, la plus importante étant la ''taille de la texture en octets'', pour éviter de déborder lors des accès à la texture.
Néanmoins, il s'agit là du cas le plus simple. Certaines techniques de rendu demandent de choisir la texture à plaquer parmi un ensemble de plusieurs textures. Les techniques en question sont assez variées et n'ont pas grand chose en commun. Les plus connues sont le ''mip-mapping'', le ''cube-mapping'' et les textures virtuelles. Le ''mip-mapping'' sert à filtrer les textures, chose qu'on expliquera plus tard, le ''cube-mapping'' sert à simuler des réflexions sur un objet en plaquant une texture de l'environnement dessus, les textures virtuelles sont une optimisation pour les textures des terrains de grande taille. Mais malgré leurs différences, elles demandent de choisir quelle texture plaquer entre plusieurs textures de base. En clair, l'adresse de base de la texture varie selon la situation. Voyons-les dans le détail.
==L'implémentation matérielle du placage de textures==
Pour résumer, la lecture d'un texel demande d'effectuer plusieurs étapes. Dans le cas le plus simple, sans ''mip-mapping'' ou ''cubemapping'', on doit effectuer les étapes suivantes :
* Il faut d'abord normaliser les coordonnées de texture pour qu'elles tombent dans l'intervalle [0,1] en fonction du mode d'adressage désiré.
* Ensuite, les coordonnées u,v doivent être converties en coordonnées entières, ce qui demande une multiplication flottante.
* Enfin, l'adresse finale est calculée à partir des coordonnées entières et en ajoutant l'adresse de base de la texture (et éventuellement avec d'autres calculs arithmétiques suivant le format de la texture).
Tout cela pourrait être fait par le pixel shaders, mais cela implique beaucoup de calculs répétitifs et d'opérations arithmétiques assez lourdes, avec des multiplications flottantes, des additions et des multiplications entières, etc. Faire faire tous ces calculs par les shaders serait couteux en performance, sans compter que les shaders deviendraient plus gros et que cela aurait des conséquences sur le cache d'instruction. De plus, certaines de ces étapes peuvent se faire en parallèle, comme les deux premières, ce qui colle mal avec l'aspect sériel des shaders.
Aussi, les processeurs de shaders incorporent une unité de calcul d'adresse spéciale pour faire ces calculs directement en matériel. L'unité de calcul en question est dans l'unité de texture. Cette dernière contient donc au minimum deux circuits : un circuit de calcul d'adresse, et un circuit d'accès à la mémoire. Toute la difficulté tient dans le calcul d'adresse, plus que dans le circuit de lecture. Le calcul d'adresse est conceptuellement réalisé en deux étapes. La première étape qui transforme les coordonnées u,v en coordonnées x,y qui donne le numéro de la ligne et de la colonne du texel dans la texture. La seconde étape prend ces deux coordonnées x,y, l'adresse de la texture, et détermine l'adresse de la tile à lire.
[[File:Unité de texture simple.png|centre|vignette|upright=2|Unité de texture simple]]
===La gestion des accès mémoire===
Enfin, l'unité de texture doit tenir compte du fait que la mémoire vidéo met du temps à lire une texture. En théorie, l'unité de texture ne devrait pas accepter de nouvelle demande de lecture tant que celle en cours n'est pas terminée. Mais faire ainsi demanderait de bloquer tout le pipeline, de l'''input assembler'' au unités de''shaders'', ce qui est tout sauf pratique et nuirait grandement aux performances.
Une solution alternative consiste à mettre en attente les demandes de lectures de texture pendant que la mémoire est occupée. La manière la plus simple d'implémenter des accès mémoire multiples est de les mettre en attente dans une petite mémoire FIFO. Cela implique que les accès mémoire s’exécutent dans l'ordre demandé par le ''shader'' et/ou l'unité de rastérisation, il n'y a pas de réorganisation des accès mémoire ou d’exécution dans le désordre des accès mémoire.
[[File:Texture prefetching.png|centre|vignette|upright=1.5|Accès mémoire simultanés.]]
Évidemment, quand la mémoire FIFO est pleine, le pipeline est alors totalement bloqué. Le rasteriser est prévenu que l'unité de texture ne peut pas accepter de nouvelle lecture de texture. En pratique, la FIFO est généralement d'une taille respectable et permet de mettre en attente beaucoup de demandes de lecture de texture. Il faut de plus noter qu'il y a une FIFO par processeur de ''shader'' sur les cartes graphiques modernes. Quand elle est pleine, le processeur cesse d'exécuter de nouveaux accès mémoire, mais peut continuer à exécuter des ''shaders'' dans les autres unités de calcul, pas besoin de bloquer complétement le pipeline.
===L'intégration du cache de textures===
Il faut noter que les unités de texture incorporent aussi un cache de texture, voire plusieurs. L'intégration des caches de texture avec la mémoire FIFO précédente est quelque peu compliqué, car il faut garantir que les lectures de texture se fassent dans le bon ordre. On ne peut pas exécuter une lecture dans le cache alors que des lectures précédentes sont en attente de lecture en mémoire vidéo. Et cela pose un gros problème : une lecture dans le cache de texture prend quelques dizaines de cycles d'horloge, alors qu'une lecture en mémoire vidéo en prend facilement 400 à 800 cycles, parfois plus. Et cela fait que l'ordre des accès mémoire peut s'inverser.
Prenons par exemple un accès au cache précédé et suivi par deux accès en mémoire vidéo. Le premier démarre au cycle 1, et se termine au cycle numéro 400. L'accès au cache commence au cycle 2 et se termine 20 cycles après, au cycle numéro 22. En clair, la lecture dans le cache s'est terminée avant l'accès mémoire qui le précède. Les textures ne sont donc plus lues dans l'ordre. Et il faut trouver une solution pour éviter cela.
La solution est de retarder les lectures dans le cache tant que tous les accès précédents ne sont pas terminés. Mais pour retarder les lectures en question, il faut d'abord savoir si la lecture atterrit dans le cache ou non, ce qui demande d'accéder au cache. On fait face à un dilemme : on veut retarder les accès au cache, mais les différencier des lectures déclenchant des accès mémoire demande d'accéder au cache en premier lieu. La solution est décrite dans l'article "Prefetching in a Texture Cache Architecture" par Igehy et ses collègues. Elle se base sur deux idées combinées ensemble.
La première idée est de séparer l'accès au cache en deux : une étape qui vérifie si les texels à lire sont dans le cache, et une étape qui accède aux données dans le cache lui-même. Un cache de texture est donc composé de deux circuits principaux. Le premier vérifie la présence des texels dans le cache. Il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Ensuite, en plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
La seconde idée est de retarder l'accès au cache entre les deux phases. La première étape d'un accès mémoire vérifie si la donnée est dans le cache ou non. Puis, on retarde la lecture des données, pour attendre que toutes les lectures précédentes soient terminées. Et enfin, troisième étape : la lecture des texels dans la mémoire cache proprement dite. Les accès mémoire passant par la mémoire vidéo se font de la même manière, à une différence près : la lecture dans le cache est remplacée par la lecture en mémoire vidéo. Tout démarre avec une demande à l'unité de tags, qui vérifie si le texel est dans le cache ou non. Puis on retarde l'accès tant que la mémoire vidéo est occupée, puis on effectue la lecture en mémoire vidéo.
Si ce n'est pas le cas, l'accès mémoire est envoyé à la mémoire vidéo comme précédemment, à savoir qu'il est mis en attente dans une mémoire FIFO, puis envoyé à la mémoire vidéo dès que celle-ci est libre. Mais en sortie de la mémoire, la donnée lue est envoyée dans le cache de texture, par dans l'unité de filtrage. Pour savoir où placer la donnée lue, l'unité de tag a réservé une ligne de cache précise, une adresse bien précise. L'adresse en question est disponible en lisant une autre mémoire FIFO, qui a mis en attente l'adresse en question, en attendant que l'accès mémoire se termine. La donnée est alors écrite dans le cache, puis lue par l'unité de filtrage de textures.
Pour une lecture dans le cache, le déroulement est similaire, mais sans le passage par la mémoire. La lecture fait une demande à l'unité de tag, et celle-ci répond que la donnée est bien dans le cache. Elle place alors l'adresse à lire dans la file d'attente. Une fois que les accès mémoire précédents sont terminés, l'adresse sort de la file d'attente et est envoyée à la mémoire de données. La lecture s'effectue, les texels sont envoyés à l'unité de filtrage de textures. La seule différence avec un ''phased cache'' normal est l'insertion de l'adresse à lire dans une FIFO qui vise à mettre en attente
[[File:Unité de texture avec un cache de texture.png|centre|vignette|upright=2.0|Unité de texture avec un cache de texture]]
Pour résumer, l'implémentation précédente garantit une exécution des lectures dans leur ordre d'arrivée. Et pour cela, elle retarde les lectures dans le cache tant que les lectures en mémoire précédentes ne sont pas terminées. L'accès au cache est plus rapide que l'accès en mémoire vidéo, mais le retard ajouté pour garantir l'ordre des lectures fait que le temps d'accès est très long.
==Le mip-mapping==
Le '''mip-mapping''' a pour but de légèrement améliorer les graphismes des objets lointains, tout en rendant les calculs de texture plus rapides. Formellement, le ''mip-mapping'' est une technique de filtrage de texture, mais nous l'abordons maintenant car elle est surtout liée au calcul d'adresse. Les unités de texture ont des circuits de filtrage de texture séparés des circuits de ''mip-mapping'' et de calcul d'adresse, d'où le fait que nous en parlons séparément.
Le problème résolu par le ''mip-mapping'' est le rendu des textures lointaines. Si une texture est plaquée sur un objet lointain, une bonne partie des détails est invisible pour l'utilisateur. Un pixel de l'écran est associé à plusieurs texels. Idéalement, la carte graphiques devrait lire tous ces texels et en faire une sorte de moyenne pondérée, pour calculer la couleur finale du pixel. Mais dans les faits, ce serait très gourmand et compliqué à implémenter en hardware. Une solution serait de ne garder que quelque texels, mais cela a tendance à créer des artefacts visuels (les textures affichées ont tendance à pixeliser). Le ''mip-mapping'' permet de réduire ces deux problèmes en même temps en précalculant cette moyenne pondérée pour des distances prédéfinies.
L'idée est d'utiliser plusieurs exemplaires d'une même texture à des résolutions différentes, chaque exemplaire étant adapté à une certaine distance. Par exemple, une texture sera stocké avec un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre de 128 * 128 et ainsi de suite jusqu’à un dernier exemplaire de 32 * 32 pixel. Chaque exemplaire correspond à un '''niveau de détail''', aussi appelé ''Level Of Detail'' (abrévié en LOD). La résolution utilisée diminue d'autant plus que l'objet est situé loin de la caméra. Les objets proches seront rendus avec la texture 512*512, ceux plus lointains seront rendus avec la texture de résolution 256*256, les textures 128*128 seront utilisées encore plus loin, et ainsi de suite jusqu'aux objets les plus lointains qui sont rendus avec la texture la plus petite de 32*32.
[[File:MipMap Example STS101.jpg|centre|vignette|upright=2|Exemples de mip-maps.]]
Le ''mip-mapping'' améliore grandement la qualité d'image. L'image d'exemple ci-dessous le montre assez bien.
[[File:Mipmapping example.png|centre|vignette|upright=2|Exemple de mipmapping.]]
Pour faciliter les calculs d'adresse, les LOD d'une même texture sont stockées les uns après les autres en mémoire (dans un tableau, comme diraient les programmeurs). Ainsi, pas besoin de se souvenir de la position en mémoire de chaque LOD : l'adresse de la texture de base, et quelques astuces arithmétiques suffisent. Prenons le cas où la texture de base a une taille L. le premier exemplaire est à l'adresse 0, le second niveau de détail est à l'adresse L, le troisième à l'adresse L + L/4, le suivant à l'adresse L + L/4 + L/16, et ainsi de suite. Le calcul d'adresse demande juste connaître le niveau de détails souhaité et l'adresse de base de la texture. Le niveau de détail voulu est calculé par les pixel shaders, en fonction de la coordonnée de profondeur du pixel à traiter.
Évidemment, cette technique consomme de la mémoire vidéo, vu que chaque texture est dupliquée en plusieurs exemplaires, en plusieurs LOD. Dans le détail, la technique du mip-mapping prend au maximum 33% de mémoire en plus (sans compression). Cela vient du fait qu'en prenant une texture dexu fois plus petite, elle prend 4 fois moins de mémoire : 2 fois moins de pixels en largeur, et 2 fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des LODs, texture de base comprise, prendra X + (X/4) + (X/16) + (X/256) + … Un petit calcul de limite donne 4/3 * X, soit 33% de plus.
===L'implémentation du mip-mapping dans l'unité de texture===
Le ''mip-mapping'' est lui aussi pris en charge par l'unité de calcul d'adresse, car cette technique change l'adresse de base de la texture. La gestion du ''mip-mapping'' est cependant assez complexe. Il est possible de laisser le pixel shader calculer quel niveau de détail utiliser, en fonction de la coordonnée de profondeur z du pixel à afficher. La carte graphique détermine alors automatiquement quelle texture lire, quel niveau de détail, automatiquement. Elle détermine aussi la bonne résolution pour la texture, qui est égal à la résolution de la texture de base, divisée par le niveau de détail. Pour résumer, le niveau de détail est envoyé aux unités de texture, qui s'occupent de calculer l'adresse de base et la résolution adéquates. Quelques calculs arithmétiques simples, donc, qui s'implémentent facilement avec quelques circuits.
Mais une autre méthode laisse la carte graphique déterminer le niveau de détail par elle-même. Dans ce cas, cela demande, outre les deux coordonnées de texture, de calculer la dérivée de ces deux coordonnées dans le sens horizontal et vertical, ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Les quatre dérivées sont les suivantes :
: <math>\frac{du}{dx}</math>, <math>\frac{dv}{dx}</math>, <math>\frac{du}{dy}</math>, <math>\frac{dv}{dy}</math>
Un bon moyen pour obtenir les dérivées demande de regrouper les pixels par groupes de 4 et de faire la différence entre leurs coordonnées de texture respectives. On peut calculer les deux dérivées horizontales en comparant les deux pixels sur la même ligne, et les deux dérivées verticales en comparant les deux pixels sur la même colonne. Mais cela demande de rastériser les pixels par groupes de 4, par ''quads''. Et c'est ce qui est fait sur les cartes graphiques actuelles, qui rastérisent des groupes de 4 pixels à la fois.
[[File:Texture sampler unit with mipmapping.png|centre|vignette|upright=2.0|Unité de texture avec mipmapping.]]
Malheureusement, le calcul exact utilisé pour le choix de la mip-map dépend du GPU considéré et peu de chose est connu quant à ces algorithmes. Il est possible d'inférer le comportement à partir d'observations, mais guère plus. Pour ceux qui veulent en savoir plus, je conseille la lecture de cet article de blog :
* [https://pema.dev/2025/05/09/mipmaps-too-much-detail/ Mipmap selection in too much detail]
==Le cube-mapping==
[[File:Cube mapped reflection example 2.JPG|vignette|Exemple de reflets environnementaux.]]
L''''environnement-mapping''' est une technique de calcul de divers effets graphiques liés à l'environnement, notamment des réflexions. L'idée est de plaquer une texture pré-calculée pour simuler l'effet de l'environnement sur une surface ou un objet 3D. Il en existe plusieurs versions différentes, mais la seule utilisée de nos jours est le ''cube-mapping'', où la texture de l'environnement est plaquée sur un cube, d'où son nom. Le cube en question est utilisé différemment suivant ce que l'on cherche à faire avec le ''cube-mapping''. Les deux utilisations principales sont le rendu du ciel et des décors, et les réflexions sur la surface des objets. Dans les deux cas, l'idée est de précalculer ce que l'on voit du point de vue de la caméra. On place la caméra dans la scène 3D, on place un cube centré sur la caméra, le cube est texturé avec ce que l'on voit de l'environnement depuis la caméra/l'objet de son point de vue.
[[File:Panorama cube map.png|centre|vignette|upright=2|L'illustration montre en premier lieu une ''cubemap'' avec les six faces mises en évidence, puis quel environnement 3D elle permet de simuler, le troisième illustration montrant comment la ''cubemap'' est utilisée pour simuler l'environnement.]]
Le rendu du ciel et des décors lointains dans les jeux vidéo se base sur des '''''skybox''''', à savoir un cube centré sur la caméra, sur lequel on ajoute des textures de ciel ou de décors lointains. Le cube est recouvert par une texture, qui correspond à ce que l'on voit quand on dirige le regard de la caméra vers cette face. Contrairement à ce qu'on pourrait croire, la skybox n'est pas les limites de la scène 3D, les limites du niveau d'un jeu vidéo ou quoique ce soit d'autre de lié à la physique de la scène 3D. La skybox est centrée sur la caméra, elle suit la caméra dans son mouvement. Centrer la skybox sur la caméra permet de simuler des décors très lointains, suffisamment lointain pour qu'on n'ait pas l'illusion de s'en rapprocher en se déplaçant dans la map. De plus, cela évite d'avoir à faire trop de calculs à chaque fois que l'on bouge la caméra. La texture plaquée sur le cube est une texture unique, elle-même découpée en six sous-textures, une par face du cube.
[[File:Skybox example.png|centre|vignette|upright=2|Exemple de Skybox.]]
[[File:Cube mapped reflection example.jpg|vignette|Réflexions calculées par une ''cubemap''.]]
Le ''cube-mapping'' est aussi utilisé pour des reflets. L'idée est de simuler les reflets en plaquant une texture pré-calculée sur l'objet réflecteur. La texture pré-calculée est un dessin de l'environnement qui se reflète sur l'objet, un dessin du reflet à afficher. En la plaquant la texture sur l'objet, on simule ainsi des reflets de l'environnement, mais on ne peut pas calculer d'autres reflets comme les reflets objets mobiles comme les personnages. Et il se trouve que la texture pré-calculée est une ''cubemap''. Pour les environnements ouverts, c'est la ''skybox'' qui est utilisée, ce qui permet de simuler les reflets dans les flaques d'eau ou dans des lacs/océans/autres. Pour les environnements intérieurs, c'est une cubemap spécifique qui utilisée. Par exemple, pour l'intérieur d'une maison, on a une ''cubemap'' par pièce de la maison. Les reflets se calculent en précisant quelle ''cubemap'' appliquer sur l'objet en fonction de la direction du regard.
[[File:Cube map level.png|centre|vignette|Cube map de l'intérieur d'une pièce d'un niveau de jeux vidéo.]]
Toujours est-il que les textures utilisées pour le ''cubemmapping'', appelées des ''cubemaps'', sont en réalité la concaténation de six textures différentes. En mémoire vidéo, la ''cubemap'' est stockée comme six textures les unes à la suite des autres. Lors du rendu, on doit préciser quelle face du cube utiliser, ce qui fait 6 possibilités. On a le même problème qu'avec les niveaux de détail, sauf que ce sont les faces d'une ''cubemap'' qui remplacent les textures de niveaux de détails. L'accès en mémoire doit donc préciser quelle portion de la ''cubemap'' il faut accéder. Et l'accès mémoire se complexifie donc. Surtout que l'accès en question varie beaucoup suivant l'API graphique utilisée, et donc suivant la carte graphique.
Les API 3D assez anciennes ne gérent pas nativement les ''cubemaps'', qui doivent être émulées en logiciel en utilisant six textures différentes. Le pixel shader décide donc quelle ''cubemap'' utiliser, avec quelques calculs sur la direction du regard. L'accès se fait d'une manière assez simple : le shader choisit quelle texture utiliser. Les API 3D récentes gèrent nativement les ''cubemaps''. Dans le cas le plus simple,pour les versions les plus vielles de ces API, les six faces sont numérotées et l'accès à une ''cubemap'' précise quel face utiliser en donnant son numéro. La carte graphique choisit alors automatiquement la bonne texture, mais cela demande de laisser le calcul de la bonne face au pixel shader. D'autres API 3D et cartes graphiques font autrement. Dans les API 3D modenres, les ''cubemap'' sont gérées comme des textures en trois dimensions, adressées avec trois coordonnées u,v,w. La carte graphique utilise ces trois coordonnées de manière à en déduire quelle est la face pertinente, mais aussi les coordonnées u,v dans la texture de la face.
==Le filtrage de textures==
Plaquer des textures sans autre forme de procès ne suffit pas à garantir des graphismes d'une qualité époustouflante. La raison est que les sommets et les texels ne tombent pas tout pile sur un pixel de l'écran : le sommet associé au texel peut être un petit peu trop en haut, ou trop à gauche, etc. Une explication plus concrète fait intervenir les coordonnées de texture. Souvenez-vous que lorsque l'on traduit une coordonnée de texture u,v en coordonnées x,y, on obtient un résultat qui ne tombe pas forcément juste. Souvent, le résultat a une partie fractionnaire. Si celle-ci est non-nulle, cela signifie que le texel/sommet n'est pas situé exactement sur le pixel voulu et que celui-ci est situé à une certaine distance. Concrètement, le pixel tombe entre quatre texels, comme indiqué ci-dessous.
[[File:Filtrage texture.png|centre|vignette|upright=2.0|Position du pixel par rapport aux texels.]]
Pour résoudre ce problème, on doit utiliser différentes techniques d'interpolation, aussi appelées techniques de '''filtrage de texture''', qui visent à calculer la couleur du pixel final en fonction des texels qui l'entourent. Il existe de nombreux types de filtrage de textures, qu'il s'agisse du filtrage linéaire, bilinéaire, trilinéaire, anisotropique et bien d'autres.
Tous ont besoin d'avoir certaines informations qui sont généralement fournies par les circuits de calcul d'adresse. La première est clairement la partie fractionnaire des coordonnées x,y. La seconde est la dérivée de ces deux coordonnées dans le sens horizontal et vertical., ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Toujours est-il que le filtrage de texture est une opération assez lourde, qui demande beaucoup de calculs arithmétiques. On pourrait en théorie le faire dans les pixels shaders, mais le cout en performance serait absolument insoutenable. Aussi, les cartes graphiques intègrent toutes un circuit dédié au filtrage de texture, le ''texture sampler''. Même les plus anciennes cartes graphiques incorporent une unité de filtrage de texture, ce qui nous montre à quel point cette opération est importante.
[[File:Texture unit.png|centre|vignette|upright=2.0|Unité de texture.]]
On peut configurer la carte graphique de manière à ce qu'elle fasse soit du filtrage bilinéaire, soit du filtrage trilinéaire, on peut configurer le niveau de filtrage anisotropique, etc. Cela peut se faire dans les options de la carte graphique, mais cela peut aussi être géré par l'application. La majorité des jeux vidéos permettent de régler cela dans les options. Ces réglages ne concernent pas la texture elle-même, mais plutôt la manière dont l'unité de texture doit fonctionner. Ces réglages sur l''''état de l'unité de texture''' sont mémorisés quelque part, soit dans l'unité de texture elle-même, soit fournies avec la ressource de texture elle-même, tout dépend de la carte graphique. Certaines cartes graphiques mémorisent ces réglages dans les unités de texture ou dans le processeur de commande, et tout changement demande alors de réinitialiser l'état des unités de texture, ce qui prend un peu de temps. D'autres placent ces réglages dans les ressources de texture elles-mêmes, ce qui rend les modifications de configuration plus rapides, mais demande plus de circuits. D'autres cartes graphiques mélangent les deux options, certains réglages étant globaux, d'autres transmis avec la texture. Bref, difficile de faire des généralités, tout dépend du matériel et le pilote de la carte graphique cache tout cela sous le tapis.
Maintenant que cela est dit, voyons quelles sont les différentes méthodes de filtrage de texture et comment la carte graphique fait pour les calculer.
===Le filtrage au plus proche===
La méthode de filtrage la plus simple consiste à colorier avec le texel le plus proche. Cela revient tout simplement à ne pas tenir compte de la partie fractionnaire des coordonnées x,y, ce qui est très simple à implémenter en matériel. C'est ce que l'on appelle le '''filtrage au plus proche''', aussi appelé ''nearest filtering''.
Autant être franc, le résultat est assez pixelisé et peu agréable à l’œil. Par contre, le résultat est très rapide à calculer, vu qu'il ne demande aucun calcul à proprement parler. Elle ne fait pas appel à la parti fractionnaire des coordonnées entières de texture, ni aux dérivées de ces coordonnées. On peut combiner cette technique avec le mip-mapping, ce qui donne un résultat bien meilleur, bien que loin d'être satisfaisant. Au passage, toutes les techniques de filtrage de texture peuvent se combiner avec du mip-mapping, certaines ne pouvant pas faire sans.
[[File:Interpolation-nearest.svg|centre|vignette|Filtrage de texture au plus proche.]]
===Le filtrage linéaire===
Le filtrage le plus simple est le '''filtrage linéaire'''. Il effectue une interpolation linéaire entre deux mip-maps, deux niveaux de détails. Pour comprendre l'idée, nous allons prendre une situation très simple, avec une texture carrée de 512 texels de côté. Le mip-mapping crée plusieurs textures : une de 256 texels de côté, une de 128 texels, une de 64, etc. Maintenant, la texture est sur un objet à une certaine distance de l'écran, vu de face. Le résultat est qu'elle correspond à l'écran à un carré de 300 pixels de côté (pas d'erreur : pixels, pas texels). Dans ce cas, la texture se trouve entre deux mip-maps : celle de 512 pixels de côté, celle de 256. Laquelle choisir ? Le filtrage au plus proche prend la texture de 512 pixels de côté. Le filtrage linéaire lui, fait autrement.
Vu que la texture est entre deux mip-maps, l'idée est de prendre le texel au plus proche dans chaque texture et de faire une sorte de moyenne appelée l'interpolation linéaire. L'interpolation par du principe que la couleur varie entre les deux texels en suivant une fonction affine, illustrée ci-dessous. Ce ne serait évidemment pas le cas dans le monde réel, mais on supposer cela donne une bonne approximation de ce à quoi ressemblerait une texture à plus haute résolution. On peut alors calculer la couleur du pixel par une simple moyenne pondérée par la distance. Le résultat est que les transitions entre deux niveaux de détails sont plus lisses, moins abruptes.
[[File:Lin interp -é.png|centre|vignette|upright=2.0|Interpolation linéaire.]]
===Le filtrage bilinéaire===
Le filtrage bilinéaire effectue une sorte de moyenne pondérée des quatre texels les plus proches du pixel à afficher. Pour cela, rappelez-vous ce qui a été dit plus haut : les coordonnées x,y d'un pixel ont une partie entière et une partie fractionnaire. Le filtrage au plus proche élimine les parties fractionnaires, ce qui donne une coordonnée x,y. Avec le filtrage bilinéaire, on prend les texels de coordonnées (x,y) ; (x+1,y) ; (x,y+1) ; (x+1,y+1), le pixel étant entre ces 4 texels.
Mais le filtrage ne fait pas qu'une simple moyenne, il prend en compte les parties fractionnaires pour faire la moyenne. En effet, le pixel n'est pas au milieu du carré de texel, il est quelque part mais est souvent plus proche d'un texel que des autres. Et il faut donc pondérer la moyenne par les distances aux 4 texels. Pour cela, la moyenne est calculée à partir d'interpolations linéaires. Avec 4 pixels, nous allons devoir calculer la couleur de deux points intermédiaires. La couleur de ces deux points se calcule par interpolation linéaire, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.
[[File:Bilin3.png|centre|vignette|upright=2|Filtrage bilinéaire de texture.]]
Le circuit qui permet de faire l'interpolation bilinéaire est particulièrement simple. On trouve un circuit de chaque pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu, et un pour la transparence. Chacun de ces circuit est composé de sous-circuits chargés d'effectuer une interpolation linéaire, reliés comme suit.
[[File:Texture sampler unit.png|centre|vignette|Unité de filtrage bilinéaire.]]
Vous noterez que le filtrage bilinéaire accède à 4 pixels en même temps. Fort heureusement, les textures sont stockées de manière à ce qu'on puisse charger les 4 pixels en une fois, comme on l'a vu plus haut. Le filtrage bilinéaire a de fortes chances que les 4 pixels filtrés soient dans la même ''tile'', la seule exception étant quand ils sont tout juste sur le bord d'une ''tile''.
: La console de jeu Nintendo 64 n'utilise que trois pixels au lieu de quatre dans son interpolation bilinéaire, qui en devient une interpolation quasi-bilinéaire. La raison derrière ce choix est une question de performances, comme beaucoup de décisions de ce genre. Le résultat est un rendu imparfait de certaines textures.
===Le filtrage trilinéaire===
Avec le filtrage bilinéaire, des discontinuités apparaissent sur certaines surfaces. Par exemple, pensez à une texture de sol : elle est appliquée plusieurs fois sur toute la surface du sol. A une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512*512 à 256*256, ce qui est visible pour un joueur attentif. De telles transitions sont lissées grâce au filtrage linéaire, il n'y a plus qu'à le combiner avec le filtrage bilinéaire. Rien d’incompatible : le premier filtre l'intérieur d'une mip-map, le second combine deux mip-maps.
Le filtrage trilinéaire prend les deux mip-maps les plus proches, fait un filtrage bilinéaire avec chacune, puis fait une « une moyenne » pondérée entre les deux résultats. Le circuit de filtrage trilinéaire existe en plusieurs versions. La plus simple, illustrée ci-dessous, effectue deux filtrages bilinéaires en parallèle, dans deux circuits séparés, puis combine leurs résultats avec un circuit d'interpolation linéaire. Mais ce circuit nécessite de charger 8 texels simultanément. Qui plus est, ces 8 texels ne sont pas consécutifs en mémoire, car ils sont dans deux niveaux de détails/mip-maps différents.
[[File:Parallel trilinear filtering.png|centre|vignette|upright=2.0|Unité de filtrage trilinéaire parallèle.]]
Vu qu'on lit des texels dans deux mip-maps, les texels sont lus en deux fois : 4 texels provenant de la première mip-map, suivis par les 4 texels de l'autre mip-map. Les 4 premiers texels doivent donc être mis en attente dans des registres, en attendant que les 4 autres arrivent. Une amélioration du circuit précédent gère cela en ajoutant des registres. Il lit les 4 premiers texels, les filtre avec une interpolation bilinéaire, et mémorise le résultat dans un registre. Puis, il lit les 4 autres texels, les filtre, et met le résultat dans un second registre. A ce moment là, un circuit d'interpolation linéaire finit le travail. On économise donc un circuit d'interpolation bilinéaire, sans que les performances soient trop impactées.
[[File:Filtrage trilineaire.png|centre|vignette|upright=1.0|Unité de filtrage trilineaire série.]]
Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le temps où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. À chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients, et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.
===Le filtrage anisotrope===
D'autres artefacts peuvent survenir lors de l'application d'une texture, la perspective pouvant déformer les textures et entraîner l'apparition de flou. La raison à cela est que les techniques de filtrage de texture précédentes partent du principe que la texture est vue de face. Prenez une texture carrée, par exemple. Vue de face, elle ressemble à un carré sur l'écran. Mais tournez la caméra, de manière à voir la texture de biais, avec un angle, et vous verrez que la forme de la texture sur l'écran est un trapèze, pas un carré. Cette déformation liée à la perspective n'est pas prise en compte par les méthodes de filtrage de texture précédentes. Pour le dire autrement, les techniques de filtrage précédentes partent du principe que les 4 texels qui entourent un pixel forment un carré, ce qui est vrai si la texture est vue de face, sans angle, mais ne l'est pas si la texture n'est pas perpendiculaire à l'axe de la caméra. Du point de vue de la caméra, les 4 texels forment un trapèze d'autant moins proche d'un carré que l'angle est grand.
Pour corriger cela, les chercheurs ont inventé le '''filtrage anisotrope'''. En fait, je devrais plutôt dire : LES filtrages anisotropes. Il en existe un grand nombre, dont certains ne sont pas utilisés dans les cartes graphiques actuelles, soit car ils trop gourmand en accès mémoires et en calculs pour être efficaces, soit car ils ne sont pas pratiques à mettre en œuvre. Il est très difficile de savoir quelles sont les techniques de filtrage de texture utilisées par les cartes graphiques, qu'elles soient récentes ou anciennes. Beaucoup de ces technologies sont brevetées ou gardées secrètes, et il faudrait vraiment creuser les brevets déposés par les fabricants de GPU pour en savoir plus. Les algorithmes en question seraient de plus difficiles à comprendre, les méthodes mathématiques cachées derrière ces méthodes de filtrage n'étant pas des plus simple.
[[File:Anisotropic filtering en.png|centre|vignette|upright=2|Exemple de filtrage anisotrope.]]
==La compression de textures==
Les textures les plus grosses peuvent aller jusqu'au mébioctet, ce qui est beaucoup. Pour limiter la casse, les textures sont compressées. La '''compression de texture''' réduit la taille des textures, ce qui peut se faire avec ou sans perte de qualité. Elle entraîne souvent une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande. Mais il s'agit là d'une technique très simple, beaucoup plus simple que les techniques que nous allons voir dans cette section. Nous allons voir quelque algorithmes de compression de textures de complexité intermédiaire, mais n'allons pas voir l'état de l'art. Il existe des formats de texture plus récents que ceux qui nous allons aborder, comme l{{'}}''Ericsson Texture Compression'' ou l{{'}}''Adaptive Scalable Texture Compression'', plus complexes et plus efficaces.
Notons que les textures sont compressées dans les fichiers du jeu, mais aussi en mémoire vidéo. Les textures sont décompressées lors de la lecture. Pour cela, la carte graphique contient alors un circuit, capable de décompresser les textures lorsqu'on les lit en mémoire vidéo. Les cartes graphiques supportent un grand nombre de formats de textures, au niveau du circuit de décompression. Du fait que les textures sont décompressées à la volée, les techniques de compression utilisées sont assez particulières. La carte graphique ne peut pas décompresser une texture entière avant de pouvoir l'utiliser dans un ''pixel shader''. A la place, on doit pouvoir lire un morceau de texture, et le décompresser à la volée. On ne peut utiliser les méthodes de compression du JPEG, ou d'autres formats de compression d'image. Ces dernières ne permettent pas de décompresser une image morceau par morceau.
Pour permettre une décompression/compression à la volée, les textures sont des textures tilées, généralement découpées en tiles de 4 * 4 texels. Les ''tiles'' sont compressées indépendamment les unes des autres. Et surtout, avec ou sans compression, la position des tiles en mémoire ne change pas. On trouve toujours une tile tous les T octets, peu importe que la tile soit compressée ou non. Par contre, une tile compressée n'occupera pas T octets, mais moins, là où une tile compressée occupera la totalité des T octets. En clair, compresser une tile fait qu'il y a des vides entre deux tiles dans al mémoire vidéo, mais ne change rien à leur place en mémoire vidéo qui est prédéterminée, peu importe que la texture soit compressée ou non. L'intérêt de la compression de textures n'est pas de réduire la taille de la texture en mémoire vidéo, mais de réduire la quantité de données à lire/écrire en mémoire vidéo. Au lieu de lire T octets pour une tile non-compressée, on pourra en lire moins.
===La palette indicée et la technique de ''Vector quantization''===
La technique de compression des textures la plus simple est celle de la '''palette indicée''', que l'on a entraperçue dans le chapitre sur les cartes d'affichage. La technique de '''''vector quantization''''' peut être vue comme une amélioration de la palette, qui travaille non pas sur des texels, mais sur des ''tiles''. À l'intérieur de la carte graphique, on trouve une table qui stocke toutes les ''tiles'' possibles. Chaque ''tile'' se voit attribuer un numéro, et la texture sera composé d'une suite de ces numéros. Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l’embarqué utilisent ce genre de compression.
===Les algorithmes de ''Block Truncation coding''===
La première technique de compression élaborée est celle du '''''Block Truncation Coding''''', qui ne marche que pour les images en niveaux de gris. Le BTC ne mémorise que deux niveaux de gris par ''tile'', que nous appellerons couleur 1 et couleur 2, les deux niveaux de gris n'étant pas le même d'une ''tile'' à l'autre. Chaque pixel d'une ''tile'' est obligatoirement colorié avec un de ces niveaux de gris. Pour chaque pixel d'une ''tile'', on mémorise sa couleur avec un bit : 0 pour couleur 1, et 1 pour couleur 2. Chaque ''tile'' est donc codée par deux entiers, qui codent chacun un niveau de gris, et une suite de bits pour les pixels proprement dit. Le circuit de décompression est alors vraiment très simple, comme illustré ci-dessous.
[[File:Block Truncation coding.jpg|centre|vignette|upright=2.0|Block Truncation coding.]]
La technique du BTC peut être appliquée non pas du des niveaux de gris, mais pour chaque composante Rouge, Vert et Bleu. Dans ces conditions, chaque ''tile'' est séparée en trois sous-''tiles'' : un sous-bloc pour la composante verte, un autre pour le rouge, et un dernier pour le bleu. Cela prend donc trois fois plus de place en mémoire que le BTC pur, mais cela permet de gérer les images couleur.
===Le format de compression S3TC / DXTC===
L'algorithme de '''Color Cell Compression''', ou CCC, améliore le BTC pour qu'il gère des couleurs autre que des niveaux de gris. Ce CCC remplace les deux niveaux de gris par deux couleurs. Une ''tile'' est donc codée avec un entier 32 bits par couleur, et une suite de bits pour les pixels. Le circuit de décompression est identique à celui utilisé pour le BTC.
[[File:Color Cell Compression.jpg|centre|vignette|Color Cell Compression.]]
[[File:Dxt1-memory-layout.png|vignette|Dxt1 et ''color cell compression''.]]
Le format de compression de texture utilisé de base par Direct X, le DXTC, est une version amliorée de l'algorithme précédent. Il est décliné en plusieurs versions : DXTC1, DXTC2, etc. La première version du DXTC est une sorte d'amélioration du CCC : il ajoute une gestion minimale de transparence, et découpe la texture à compresser en ''tiles'' de 4 pixels de côté. La différence, c'est que la couleur finale d'un texel est un mélange des deux couleurs attribuée au bloc. Pour indiquer comment faire ce mélange, on trouve deux bits de contrôle par texel.
Si jamais la couleur 1 < couleur2, ces deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (2 * Couleur1 + Couleur2) / 3
* 11 = (Couleur1 + 2 * Couleur2) / 3
Sinon, les deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (Couleur1 + Couleur2) / 2
* 11 = Transparent
[[File:DXTC.jpg|centre|vignette|DXTC.]]
Le circuit de décompression du DXTC ressemble alors à ceci :
[[File:Circuit de décompression du DXTC.jpg|centre|vignette|upright=2.0|Circuit de décompression du DXTC.]]
===Les format DXTC 2, 3, 4 et 5 : l'ajout de la transparence===
Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3, lui-même replacé par le DXT4 et par le DXT5. Dans le DXT3, la transparence fait son apparition. Pour cela, on ajoute 64 bits par ''tile'' pour stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1.
[[File:Dxt23-memory-layout.png|centre|vignette|Dxt 2 et 3.]]
Dans le DXT4 et le DXT5, la méthode utilisée pour compresser les couleurs l'est aussi pour les valeurs de transparence. L'information de transparence est stockée par un en-tête contenant deux valeurs de transparence, le tout suivi d'une matrice qui attribue trois bits à chaque texel. En fonction de la valeur des trois bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1.
[[File:Dxt45-memory-layout.png|centre|vignette|Dxt 4 et 5.]]
===Le format de compression PVRTC===
Passons maintenant à un format de compression de texture un peu moins connu, mais pourtant omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle travaille surtout dans les cartes graphiques embarquées. Ses cartes se trouvent notamment dans l'ipad, l'iPhone, et bien d'autres smartphones actuels.
Avec le PVRTC, les textures sont encore une fois découpées en ''tiles'' de 4 texels par 4, mais la ressemblance avec le DXTC s’arrête là. Chacque ''tile'' est codée avec :
* une couleur codée sur 16 bits ;
* une couleur codée sur 15 bits ;
* 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
* et un bit de modulation, qui permet de configurer l’interprétation des bits de mélange.
Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4*4.
==Annexe : le ''normal-mapping'' hardware==
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, parlons un peu du ''normal mapping'' et de son implémentation dans les unités de texture. Pour rappel, 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. Et pour comprendre quel est le rapport avec les textures, nous allons devoir faire quelques rappels sur l'éclairage par pixel.
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
L'éclairage se fait en utilisant de nombreux calculs, qu'on a détaillé dans le chapitre sur les bases du rendu 3D. Ceux-ci utilisent la normale d'un sommet, à savoir un vecteur orienté à la verticale de la surface. En théorie, il y a une normale par sommet, ce qui fait que les calculs d'éclairage doivent se faire au niveau géométrique, avant la rastérisation. Mais pour obtenir un éclairage de meilleure qualité, il y a des techniques qui calcule l'éclairage d'une scène 3D pixel par pixel.
Une première technique d'éclairage par pixel interpole les normales lors de l'étape de rastérisation. On obtient alors un éclairage de Phong. Une autre solution est celle du ''normal mapping''. Le '''''normal mapping''''' précalcule les normales d'une surface dans une texture, appelée la ''normal map''. L'éclairage est alors réalisé par un pixel shader, qui lit les normales depuis cette texture et fait les calculs d'éclairage avec.
===L'usage de textures non-compressées pour les ''normal maps''===
La ''normal map'' est une texture, donc. Mais ce n'est pas une texture comme une autre. Elle mémorise un vecteur pour chaque texel, pas une couleur. Il est possible d'utiliser les formats de textures non-compressés, même si cela ne donne pas de bons résultats. Après tout, un vecteur est codé par trois coordonnées x,y,z, il est possible de coder chacune avec un octet et de packager cela dans une texture RGB classique. La coordonnée x va dans la composante Rouge, la coordonnée Y dans la composante Bleu, et la coordonnée z dans la composante z.
Une première optimisation est de ne pas mémoriser les trois coordonnées, mais seulement deux d'entre elles et de calculer la troisième. En effet, les normales sont des vecteurs ''normalisés'', c'est à dire que leur longueur vaut 1, par construction. Vu que la taille est connue à l'avance, on peut en déduire la coordonnée z à partir des coordonnées x et y, avec l'usage du théorème de Pythagore. Par définition, <math>z^2 = 1 - x^2 - y^2</math>. Le calcul peut être fait dans le ''pixel shader'' sans problème.
: Pour être précis, il faut que les normales soient définies d'une certaine manière pour que ça marche. Les normales ne sont pas définies dans le même système de coordonnées que le modèle 3D, mais dans un autre système appelé l'''espace tangent''.
Notons que l'accès à la ''normal map'' se fait comme pour n'importe quelle texture : les texels sont lus en mémoire vidéo, puis le filtrage de texture est appliqué, et enfin le tout est envoyé au ''pixel shader''. En théorie, le filtrage effectue une sorte d'interpolation des normales automatique, ce qui permet de trouver la normale à un pixel précis, même s'il n'est pas sur un texel.
Les solutions précédentes demandent d'utiliser des textures non-compressées, qui utilisent beaucoup de mémoire vidéo. Utiliser des textures compressées pourrait sembler résoudre le problème. Mais l'usage de textures compressées marche très mal pour les ''normal maps'', elles ne permettent pas d'obtenir une qualité ou une compression suffisante. Aussi, des formats de texture dédiés aux ''normal maps'' ont été inventés.
===Le format de texture 3Dc===
Le problème est que le rendu final n'est pas très beau. Et les opérations de filtrage ne donnent pas de très bons résultats. Pour remédier à ces problèmes, des formats de texture spécialisés pour les ''normal map'' ont été inventés. Le premier d'entre eux est le '''3Dc''', aussi appelé BC5 pour ''Block Compression 5''. Il utilise un octet par texel, au lieu de trois avec une texture RGB normale non-compressée. Et surtout : il est géré en matériel, directement dans l'unité de texture, qui peut décompresser les textures 3Dc et les filtrer !
Avec le 3Dc, seules coordonnées sont mémorisées, la troisième est calculée, comme dit plus haut. Pour le reste, la compression de la ''normal map'' ressemble un peu à celle des textures. La ''normal map'' est découpée en blocs de 4 par 4 texels de côté, chacune étant encodée en tenant compte de certaines redondances. Dans un bloc, les coordonnées x sont dans un certain intervalle, allant de <math>x_{min}</math> à <math>x_{max}</math>, idem pour la coordonnée y qui est dans l'intervalle <math>y_{min}</math>, <math>y_{max}</math>. L'intervalle <math>x_{min}</math> à <math>x_{max}</math> est coupé en 8 parts égales, ce qui fait 8 valeurs possibles dans cet intervalle, chacune correspondant à une valeur x possible pour une normale du bloc. La normale est donc encodée en précisant quelle valeur est la bonne, en utilisant un indice de 3 bits pour cela. La valeur exacte de la coordonnée x se calcule à partir de l'indice comme suit :
: <math>x = x_{min} + \left( \frac{x_{min} - x_{min}}{8} \right) \times \text{indice} </math>
Pour encoder un bloc de 4 par 4 normales, il faut donc :
* Un octet pour chaque coordonnée <math>x_{min}</math>, <math>x_{max}</math>, <math>y_{min}</math>, <math>y_{max}</math>.
* 6 bits par normale : 3 pour encoder le décalage de la coordonnée x, 3 pour la coordonnée y.
Le tout permet d'encoder un bloc de 16 normales en seulement 128 bits, soit un octet par normale. Le ''pixel shader'' se débrouille pour décompresser un bloc. Il effectue notamment le calcul du dessus, pour retrouver la coordonnée x. Les calculs étant particulièrement simples, le cout en performance est très faible. Et ce d'autant plus qu'ils demandent juste de faire des additions et des multiplications entières, que l'unité scalaire peut faire en parallèle d'autres opérations plus gourmandes. Et le cout en calculs est de toute façon compensé par l'économie de mémoire et de bande passante lié à la compression : diviser la taille des données par trois, ca a un sacré impact !
==Annexe : les ''shadowmap'' hardware==
Les anciens GPU, notamment la Geforce FX, avaient des fonctionnalités spécifiques pour le calcul des ombres. Dans la plupart des jeux vidéos de l'époque, et même de maintenant, les ombres sont calculées avec la technique des ''shadowmap''. L'idée est assez simple sur le principe : un pixel est dans l'ombre quand il est invisible depuis une source de lumière.
L'idée est que le rendu est réalisé en plusieurs passes, avec une passe par source de lumière et une passe finale pour calculer l'image finale. Nous allons expliquer la technique avec une seule source de lumière, et allons utiliser l'exemple de la scène ci-dessous.
[[File:7fin.png|centre|vignette|Scène 3D d'exemple.]]
===La technique du ''shadowmapping''===
[[File:2shadowmap.png|vignette|Résultat de la première passe : ''shadowmap''..]]
La première passe rend l'image depuis le point de vue de la source de lumière. Cette première passe ne rend pas les couleurs de la scène, elle ne s'intéresse qu'à la profondeur des pixels. Le résultat est que l'image ne rend que le tampon de profondeur. Celui-ci est ensuite réutilisé comme texture pour la passe suivante. La texture en question est appelée la '''''shadownmap'''''.
La perspective utilisée, ainsi que le ''view frustrum'', dépend de la source de lumière. Pour une source de lumière qui émet un cône de lumière, le ''view frustrum'' de l'image rendue doit contenir tout le cone de lumière, et doit coller le plus possible à celui-ci. Pour une source directionnelle, comme le soleil, une perspective orthographique est utilisée.
La seconde passe rend l'image du point de vue de la caméra, pour rendre l'image finale. Elle rend l'image finale, qui est composée de pixels, chacun ayant une position à l'écran x,y, et une profondeur z. Les coordonnées sont transformées pour obtenir la position de ce pixel depuis le point de vue de la caméra. Une simple multiplication de matrice suffit, rien de bien compliqué, un shader peut le faire.
[[File:5failed.png|vignette|Résultat du test des comparaisons.]]
Après cette étape, on a alors les coordonnées x,y,z de ce pixel du point de vue de la caméra, et la ''shadowmap''. Il est alors possible d'accéder à la ''shadowmap'' au même endroit, à la même place que le pixel testé, aux mêmes coordonnées x,y. Si la profondeur du pixel est supérieure à celle de la shadowmap au même endroit, alors le pixel est situé derrière la surface visible, donc est dans l'ombre. Sinon, il n'est pas dans l'ombre. Le même procédé est répété sur chaque pixel de l'écran.
===Les optimisations hardware du ''shadowmapping''===
La technique des ''shadowmap'' demande donc de calculer une texture ''shadowmap'', puis de lire celle-ci et de faire des comparaisons de profondeur. Les GPU comme la Geforce FX intégraient du matériel dans les unités de texture pour faciliter ce travail. Les unités de texture pouvaient lire les ''shadowmap'', et faire la comparaison de profondeur toutes seules, elles avaient des circuits pour. Il suffisait de leur fournir le pixel à tester, ses coordonnées x,y,z, et l'adresse de la ''shadowmap''. Les unités de texture renvoyaient alors un résultat valant 0 ou 1 : 1 si le pixel est dans l'ombre, 0 sinon.
Elles pouvaient même effectuer du filtrage de texture sur les ''shadowmap''. Mais le filtrage était différent de celui utilisé sur les autres textures : moyenner des valeurs de profondeur ne marche pas bien. Elles utilisaient des techniques de filtrage différentes : elles faisaient les tests de comparaison, puis faisaient la moyenne des résultats. Ainsi, pour du filtrage bilinéaire, elles lisaient 4 texels dans la ''shadowmap'', puis faisaient 4 tests de comparaison, et moyennaient les 4 résultats.
==Annexe : les textures virtuelles==
Les '''textures virtuelles''' sont une optimisation des textures normales, qui visent à accélérer le rendu de terrains de grande taille. Imaginez par exemple un monde assez ouvert, comme un environnement en forêt ou en montagne, avec une grande distance de visibilité. Avec de tels terrains, le "sol" est recouvert par une texture de sol unique qui recouvre tout le terrain. Elle ne se répète pas, est de très grande taille, et peut parfois recouvrir toute la map ! Mais il n'y a pas assez de mémoire vidéo pour mémoriser la texture toute entière. La seule solution est la suivante : une partie de la texture est placée en mémoire vidéo, le reste est soit placé en mémoire RAM ou sur le disque dur.
Pour cela, le moteur de jeu utilise une optimisation ingénieuse, basée sur une observation assez basique : une bonne partie de la texture est visible, mais le reste est caché par des arbres, des habitations ou d'autres obstacles. Une optimisation possible de ne garder en mémoire vidéo que les portions visibles de la texture, pas les portions cachées. Une autre optimisation mélange textures virtuelles et ''mip-mapping''. L'idée est que pour les portions lointaines d'une texture, la texture utilisée est une ''mip-map'' de basse résolution. L'idée est alors de ne charger que la ''mip-map'' adéquate, pas les autres niveaux de détail. En clair, la texture de base n'est pas chargée en mémoire vidéo, mais la ''mip-map'' basse résolution l'est.
===Une texture à deux niveaux===
L'implémentation des textures virtuelles découpe les méga-textures en ''tiles'', en morceaux rectangulaires de taille modeste. En clair, le terrain est découpé en morceau rectangulaires/carrés. Seules les tiles nécessaires sont chargées en mémoire vidéo, pas les autres. Par exemple, les ''tiles'' non-visibles ne sont pas placées en mémoire vidéo, seules les ''tiles'' visibles le sont. De même, il y a une ''tile'' par niveau de mip-map : seul la tile correspondant le niveau adéquat est en mémoire vidéo, les autres niveaux de détail ne sont pas chargés. On peut faire une analogie avec la mémoire virtuelle, où les données sont découpées en pages, qui sont chargées en mémoire RAM à la demande, suivant les besoins, les données pouvant être swappées sur le disque dur si elles sont peu utilisées. Sauf qu'ici, il s'agit de textures qui sont découpées en pages chargées à la demande en mémoire vidéo, depuis la RAM système.
Une texture virtuelle est en réalité un système à deux niveaux : une liste de ''tiles'' et les ''tiles'' elles-mêmes. La liste de ''tiles'' est appelée un '''atlas de texture''', c'est un peu l'équivalent de la ''tilemap'' pour le rendu 2D. Rendre une texture demande de calculer quelle ''tile'' contient le texel à afficher, consulter la ''tile'' en question, puis récupérer le texel adéquat dans cette ''tile''. La ''tile'' est donc une texture, mais la texture à charger est choisie parmi un ensemble, qui est ici l'atlas de texture.
===L'implémentation : logicielle versus matérielle===
Les textures virtuelles ont été utilisées pour la première fois par les jeux Rage 1 et 2 d'IdSoftware, et quelques jeux ultérieurs comme DOOM 2016. IdSoftware les appelait des '''''mega-textures'''''. L'optimisation permettait des gains en performance assez impressionnants. Le jeu Rage 1 utilisait une texture carrée unique de 128k pixels de côté pour rendre le terrain. En théorie, une telle texture devrait prendre 64 giga-octets, mais le jeu tournait correctement avec 512 méga-octets de RAM, poussivement avec seulement 256 méga-octets de RAM.
De nos jours, les textures virtuelles sont supportées par beaucoup de jeux vidéos, les moteurs les plus courants gèrent de telles textures de manière logicielles. Mais quelques GPU récents supportent les textures virtuelles. Sur les GPU récents, l'atlas de texture est géré nativement par le matériel. Le GPU choisit quelle ''tile'', quelle texture choisir pour rendre le texel adéquat. Pour cela, le GPU calcule quelle ''tile'' charger, consulte l'atlas de texture, et lit la texture de ''tile'' adéquate.
Mais l'implémentation sur les GPU récents a de nombreuses limitations. La limitation la plus importante est que la taille des textures virtuelles ne peut pas dépasser la taille d'une texture normale, soit 32768 pixels de côté pour une texture carrée environ sur les GPU de 2020. De plus, le chargement d'une ''tile'' est très lent. En clair, dès qu'on veut changer de niveau de mip-map pour une tile, ou dès qu'une tile devient visible, le chargement de la tile peut facilement prendre plusieurs centaines de millisecondes. Le filtrage de texture est très complexe avec des textures virtuelles, ce qui fait que le filtrage de texture virtuelle est souvent soumis à des limitations que les textures normales n'ont pas, notamment pour le filtrage anisotropique.
{{NavChapitre | book=Les cartes graphiques
| prev=Le rasterizeur
| prevText=Le rasterizeur
| next=Les Render Output Target
| nextText=Les Render Output Target
}}{{autocat}}
cac8h3nbzbb7s05500r01dft9uf9jwd
763420
763419
2026-04-10T19:27:38Z
Mewtow
31375
/* Le cube-mapping */
763420
wikitext
text/x-wiki
[[File:Texture mapping.png|vignette|''Texture mapping'']]
Les '''textures''' sont des images que l'on va plaquer sur la surface d'un objet, du papier peint en quelque sorte. Les cartes graphiques supportent divers formats de textures, qui indiquent comment les pixels de l'image sont stockés en mémoire : RGB, RGBA, niveaux de gris, etc. Une texture est donc composée de "pixels", comme toute image numérique. Pour bien faire la différence entre les pixels d'une texture, et les pixels de l'écran, les pixels d'une texture sont couramment appelés des ''texels''.
==Le placage de textures inverse==
Pour rappel, plaquer une texture sur un objet consiste à attribuer un texel à chaque sommet, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture.
Dans les faits, on n'utilise pas de coordonnées entières de ce type. Les coordonnées de texture sont deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. Le nom donnée à cette technique de description des coordonnées de texture s'appelle l''''''UV Mapping'''''.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Les API 3D modernes gèrent des textures en trois dimensions, ce qui ajoute une troisième coordonnée de texture notée w. Dans ce qui va suivre, nous allons passer les textures en trois dimensions sous silence. Elles ne sont pas très utilisées, la quasi-totalité des jeux vidéo et applications 3D utilisant des textures en deux dimensions. Par contre, le matériel doit gérer les textures 3D, ce qui le rend plus complexe que prévu. Il faut ajouter quelques circuits pour, de quoi gérer la troisième coordonnée de texture, etc.
Lors de la rastérisation, chaque fragment se voit attribuer un sommet, et donc la coordonnée de texture qui va avec. Si un pixel est situé pile sur un sommet, la coordonnée de texture de ce sommet est attribuée au pixel. Si ce n'est pas le cas, la coordonnée de texture finale est interpolée à partir des coordonnées des trois sommets du triangle rastérisé. L'interpolation en question a lieu dans l'étape de rastérisation, comme nous l'avons vu dans le chapitre précédent. Le fait qu'il y ait une interpolation fait que les coordonnées du pixel gagent à être des nombres flottants. On pourrait faire une interpolation avec des coordonnées de texture entières, mais les arrondis et autres imprécisions de calcul donneraient un résultat graphiquement pas terrible, et empêcheraient d'utiliser les techniques de filtrage de texture que nous verrons dans ce chapitre.
À partir de ces coordonnées de texture, la carte graphique calcule l'adresse du texel qui correspond, et se charge de le lire. Et toute la magie a lieu dans ce calcul d'adresse, qui part de coordonnées de texture flottante, pour arriver à une adresse mémoire. Le calcul de l'adresse du texel se fait en plusieurs étapes, que nous allons voir ci-dessous. La première étape convertit les coordonnées flottantes en coordonnées entières, qui disent à quel ligne et colonne se trouve le texel voulu dans la texture. L'étape suivante transforme ces coordonnées x,y entières en adresse mémoire.
===La normalisation des coordonnées===
J'ai dit plus haut que les coordonnées de texture sont des coordonnées flottantes, comprises entre 0 et 1. Mais il faut savoir que les pixels shaders peuvent modifier celles-ci pour mettre en œuvre certains effets graphiques. Et le résultat peut alors se retrouver en-dehors de l'intervalle 0,1. C'est quelque chose de voulu et qui est traité par la carte graphique automatiquement, sans que ce soit une erreur. Au contraire, la manière dont la carte graphique traite cette situation permet d'implémenter des effets graphiques comme des textures en damier ou en miroir.
[[File:Clamp tile.jpg|vignette|Clamp tile]]
Il existe globalement trois méthodes très simples pour gérer cette situation, qui sont appelés des '''modes d'adressage de texture'''.
* La première méthode est de faire en sorte que le résultat sature. Si une coordonnée est inférieur à 0, alors on la remplace par un zéro. Si elle est supérieure à 1, on la ramène à 1. Avec cette méthode, tout se passe comme si les bords de la texture étaient étendus et remplissaient tout l'espace autour de la texture. Le tout est illustré ci-dessous. Ce mode d'accès aux textures est appelé le '''''clamp'''''.
* Une autre solution retire la partie entière de la coordonnée, elle coupe tout ce qui dépasse 1. Pour le dire autrement, elle calcule le résultat modulo 1 de la coordonnée. Le résultat est que tout se passe comme si la texture était répétée à l'infini et qu'elle pavait le plan.
* Une autre méthode remplit les coordonnées qui sortent de l’intervalle 0,1 avec une couleur préétablie, configurée par le programmeur.
===La conversion des coordonnées de textures flottantes en adresse mémoire===
Une fois la normalisation effectuée, les coordonnées de texture sont utilisées pour lire le texel voulu. Pour cela, les coordonnées de texte sont transformées en adresse mémoire, adresse qui pointe sur le texel ayant ces cordonnées. Pour cela, la première étape est de transformer les coordonnées flottantes u,v en coordonnées entières x,y qui pointent sur un texel. Pour cela, il suffit de multiplier les coordonnées flottantes u,v par la résolution de la texture accédée. Pour un écran de résolution <math>\text{height,width}</math>, le calcul est le suivant :
: <math>x = u \times \text{width}</math>
: <math>y = v \times \text{height}</math>
Le résultat est un nombre avec une partie entière et une partie fractionnaire. La partie entière des deux coordonnées donne la position x,y voulue, et la partie fractionnaire est conservée pour le filtrage de textures, mais passons cela sous silence pour le moment.
La seconde étape prend les coordonnées entières x,y et calcule l'adresse mémoire du texel. L'adresse dépend de la position de la texture en mémoire, précisément de son début, son premier texel, mais aussi de la position du texel par rapport au début de la texture. Et calculer cette position intra-texture dépend de la manière dont les texels sont stockés en mémoire.
====Les textures naïves====
Les programmeurs qui lisent ce cours s'attendent certainement à ce que la texture soit stockée en mémoire ligne par ligne, ou colonne par colonne. Cela veut dire que le premier pixel en partant d'en haut à gauche est stocké en premier, puis celui immédiatement à sa droite, puis celui encore à droite, et ainsi de suite. Une fois qu'on arrive à la fin d'une ligne, on passe à la ligne suivante, en-dessous. Cette organisation ligne par ligne s'appele l'organisation '''''row major order'''''. On peut faire pareil, mais colonne par colonne, ce qui donne le '''''column major order'''''.
[[File:Speicheranordnung Feld.svg|centre|vignette|upright=2|Row et column major order.]]
Maintenant, supposons que la texture commence à l'adresse <math>A_\text{texture}</math>, qui est l'adresse du premier texel. La texture a une résolution de <math>\text{width}</math> texels de large et <math>\text{height}</math> texels de haut. Par définition, les coordonnées X et Y des texels commencent à 0, ce qui fait que le pixel en haut à gauche a les coordonnées 0,0.
L'adresse du pixel se calcule comme suit :
: <math>A_\text{pixel} = A_\text{texture} + (\text{taille d'une ligne en octets} \times Y) + (\text{taille d'un texel en octets} \times X)</math>
La taille d'un pixel en mémoire est notée T. La taille d'une ligne en mémoire est de <math>width \times T</math>, par définition, vu qu'elle fait <math>width</math> texels. On a donc :
: <math>A_\text{pixel} = A_\text{texture} + (width \times T \times Y) + (T \times X)</math>
La formule se réécrit comme suit :
: <math>A_\text{pixel} = A_\text{texture} + T \times (width \times Y + X)</math>
Le calcul d'adresse est donc assez simple. Malheureusement, les textures ne sont pas stockées de cette manière en mémoire vidéo. En effet, elle se marie mal avec les opérations de filtrage de texture que nous allons voir dans ce qui suit. Le filtrage d'un texel dépend de ses voisins du dessus et du dessous. Le fait que la texture n'est pas forcément parcourue ligne par ligne fait que stocker une texture ligne par ligne n'est pas l'idéal.
De même, les textures sont déformées par la perspective. L'affichage de la texture ne se fait alors pas ligne par ligne, mais en parcourant la texture en diagonale, l'angle de la diagonale correspondant approximativement à l'angle que fait la verticale de la texture avec le regard. Vu qu'on ne connait pas à l'avance l'angle que fera la diagonale de parcours, on doit ruser.
====Les textures tilées====
Une première solution à ce problème est celle des '''textures tilées'''. Avec ces textures, l'image de la texture est découpée en ''tiles'', des rectangles ou en carrés de taille fixe, généralement des carrés de 4 pixels de côté. Les tiles ont une largeur et une longueur égales, afin de simplifier les calculs : on divise X et Y par le même nombre. De plus, leur largeur et leur longueur sont une puissance de deux, afin de simplifier les calculs d'adresse. Les ''tiles'' sont alors mémorisée les unes après les autres dans le fichier de la texture.
[[File:Texture tilée.png|centre|vignette|upright=2|Texture tilée]]
La formule de calcul d'adresse vue plus haut doit être adaptée pour tenir compte des tiles. Pour cela, il faut remplacer la taille d'un texel par la taille d'une tile, et que la largeur de la texture soit exprimée en nombre de tiles. De plus, on doit adapter les coordonnées des texels pour donner des coordonnées de tile. Généralement, les tiles sont des carrés de N pixels de côté, ce qui fait qu'on peut regrouper les lignes et les colonnes par paquets de N. Il suffit donc de diviser Y et X pour obtenir les coordonnées de la tile, de même que la larguer. La formule pour calculer la position de la énième tile est alors la suivante :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + \text{Taille mémoire d'une tile} \times \left( {\text{Width} \over N} \times {Y \over N} + {X \over N} \right)</math>
On peut réécrire le tout comme suit :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + K \times \left( {Y \over N} + X \right)</math>, avec K une constante connue à la compilation des shaders.
Vu que les tiles sont carrées avec une largeur qui est une puissance de deux, la multiplication par la taille d'une tile en mémoire se simplifie : on passe d'une multiplication entière à des décalages de bits. Même chose pour le calcul de l'adresse de la tile à partir des coordonnées x,y : ils impliquent des divisions par une puissance de deux, qui deviennent de simples décalages.
La position d'un pixel dans une tile dépend du format de la texture, mais peut se calculer avec quelques calculs arithmétiques simples. Dans les cas les plus simples, les pixels sont mémorisés ligne par ligne, ou colonne par colonne. Mais ce n'est pas systématiquement le cas. Toujours est-il que les calculs pour déterminer l'adresse sont simples, et ne demandent que quelques additions ou multiplications. Mais avec les formats de texture utilisés actuellement, les tiles sont chargées en entier dans le cache de texture, sans compter que diverses techniques de compression viennent mettre le bazar, comme on le verra dans la suite de cours.
Un avantage de l'organisation en tiles est qu'elle se marie bien avec le parcours des textures. On peut parcourir une texture dans tous les sens, horizontal, vertical, ou diagonal, on sait que les prochains pixels ont de fortes chances d'être dans la même tile. Si on rentre dans une tile par la gauche en haut, on a encore quelques pixels à parcourir dans la tile, par exemple. De même, le filtrage de textures est facilité. On verra dans ce qui va suivre que le filtrage de texture a besoin de lire des blocs de 4 texels, des carrés de 2 pixels de côté. Avec l'organisation en tile, on est certain que les 4 texels seront dans la même tile, sauf s'ils ont le malheur d'être tout au bord d'une tile. Ce dernier cas est assez rare, et il l'est d'autant plus que les tiles sont grandes. Enfin, un dernier avantage est que les tiles sont généralement assez petites pour tenir tout entier dans une ligne de cache. Le cache de texture est donc utilisé à merveille, ce qui rend les accès aux textures plus rapides.
====Les textures basées sur des ''z-order curves''====
Les formats de textures théoriquement optimaux utilisent une '''''Z-order curve''''', illustrée ci-dessous. L'idée est de découper la texture en quatre rectangles identiques, et de stocker ceux-ci les uns à la suite des autres. L'intérieur de ces rectangles est lui aussi découpé en quatre rectangles, et ainsi de suite. Au final, l'ordre des pixels en mémoire est celui illustré ci-dessous.
[[File:Z-CURVE.svg|centre|vignette|upright=2|Construction d'une ''Z-order curve''.]]
Les texels sont stockés les uns à la suite des autres dans la mémoire, en suivant l'ordre donnée par la ''Z-order curve''. Le calcul d'adresse calcule la position du texel en mémoire, par rapport au début de la texture, et ajoute l'adresse du début de la texture. Mais tout le défi est de calculer la position d'un texel en mémoire, à partir des coordonnées x,y. Le calcul peut sembler très compliqué, mais il n'en est rien. Le calcul demande juste de regarder les bits des deux coordonnées et de les combiner d'une manière particulièrement simple. Il suffit de placer le bit de poids fort de la coordonnée x, suivi de celui de la coordonnée y, et de faire ainsi de suite en passant aux bits suivants.
[[File:Zcurve45bits.png|centre|vignette|upright=1.5|Calcul de la position d'un élément dans une ''Z-order curve'' à partir des coordonnées x et y.]]
L'avantage d'une telle organisation est que la textures est découpées en ''tiles'' rectangulaires d'une certaine taille, elles-mêmes découpées en ''tiles'' plus petites, etc. Et il se trouve que cette organisation est parfaite pour le cache de texture. L'idéal pour le cache de texture est de charger une ''tile'' complète dans le cache de textures. Quand on accède à un texel, on s'assure que la ''tile'' complète soit chargée. Mais cela demande de connaitre à l'avance la taille d'une ''tile''. Les formats de texture fournissent généralement une ''tile'' carré de 4 pixels de côté, mais cela donnerait un cache trop petit pour être vraiment utile. Avec cette méthode, on s'assure qu'il y ait une ''tile'' avec la taille optimale. Les ''tiles'' étant découpées en ''tiles'' plus petites, elles-mêmes découpées, et ainsi de suite, on s'assure que la texture est découpées en ''tiles'' de taille variées. Il y aura au moins une ''tile'' qui rentrera tout pile dans le cache.
===Les techniques de rendu à textures multiples===
Nous venons de voir comment une texture est plaquée sur un objet 3D, ou une surface comme un sol. Pour résumer, le calcul de l'adresse d'un texel prend la position du texel par rapport au début de la texture, et ajoute l'adresse du début de la texture. L'adresse mémoire de la texture est connue au moment où le pilote de la carte graphique place la texture dans la mémoire vidéo, et cette information est transmise au matériel par l'intermédiaire du processeur de commande, puis passée aux processeurs de shaders et à l'unité de texture. Le tout est couplé à d'autres informations, la plus importante étant la ''taille de la texture en octets'', pour éviter de déborder lors des accès à la texture.
Néanmoins, il s'agit là du cas le plus simple. Certaines techniques de rendu demandent de choisir la texture à plaquer parmi un ensemble de plusieurs textures. Les techniques en question sont assez variées et n'ont pas grand chose en commun. Les plus connues sont le ''mip-mapping'', le ''cube-mapping'' et les textures virtuelles. Le ''mip-mapping'' sert à filtrer les textures, chose qu'on expliquera plus tard, le ''cube-mapping'' sert à simuler des réflexions sur un objet en plaquant une texture de l'environnement dessus, les textures virtuelles sont une optimisation pour les textures des terrains de grande taille. Mais malgré leurs différences, elles demandent de choisir quelle texture plaquer entre plusieurs textures de base. En clair, l'adresse de base de la texture varie selon la situation. Voyons-les dans le détail.
==L'implémentation matérielle du placage de textures==
Pour résumer, la lecture d'un texel demande d'effectuer plusieurs étapes. Dans le cas le plus simple, sans ''mip-mapping'' ou ''cubemapping'', on doit effectuer les étapes suivantes :
* Il faut d'abord normaliser les coordonnées de texture pour qu'elles tombent dans l'intervalle [0,1] en fonction du mode d'adressage désiré.
* Ensuite, les coordonnées u,v doivent être converties en coordonnées entières, ce qui demande une multiplication flottante.
* Enfin, l'adresse finale est calculée à partir des coordonnées entières et en ajoutant l'adresse de base de la texture (et éventuellement avec d'autres calculs arithmétiques suivant le format de la texture).
Tout cela pourrait être fait par le pixel shaders, mais cela implique beaucoup de calculs répétitifs et d'opérations arithmétiques assez lourdes, avec des multiplications flottantes, des additions et des multiplications entières, etc. Faire faire tous ces calculs par les shaders serait couteux en performance, sans compter que les shaders deviendraient plus gros et que cela aurait des conséquences sur le cache d'instruction. De plus, certaines de ces étapes peuvent se faire en parallèle, comme les deux premières, ce qui colle mal avec l'aspect sériel des shaders.
Aussi, les processeurs de shaders incorporent une unité de calcul d'adresse spéciale pour faire ces calculs directement en matériel. L'unité de calcul en question est dans l'unité de texture. Cette dernière contient donc au minimum deux circuits : un circuit de calcul d'adresse, et un circuit d'accès à la mémoire. Toute la difficulté tient dans le calcul d'adresse, plus que dans le circuit de lecture. Le calcul d'adresse est conceptuellement réalisé en deux étapes. La première étape qui transforme les coordonnées u,v en coordonnées x,y qui donne le numéro de la ligne et de la colonne du texel dans la texture. La seconde étape prend ces deux coordonnées x,y, l'adresse de la texture, et détermine l'adresse de la tile à lire.
[[File:Unité de texture simple.png|centre|vignette|upright=2|Unité de texture simple]]
===La gestion des accès mémoire===
Enfin, l'unité de texture doit tenir compte du fait que la mémoire vidéo met du temps à lire une texture. En théorie, l'unité de texture ne devrait pas accepter de nouvelle demande de lecture tant que celle en cours n'est pas terminée. Mais faire ainsi demanderait de bloquer tout le pipeline, de l'''input assembler'' au unités de''shaders'', ce qui est tout sauf pratique et nuirait grandement aux performances.
Une solution alternative consiste à mettre en attente les demandes de lectures de texture pendant que la mémoire est occupée. La manière la plus simple d'implémenter des accès mémoire multiples est de les mettre en attente dans une petite mémoire FIFO. Cela implique que les accès mémoire s’exécutent dans l'ordre demandé par le ''shader'' et/ou l'unité de rastérisation, il n'y a pas de réorganisation des accès mémoire ou d’exécution dans le désordre des accès mémoire.
[[File:Texture prefetching.png|centre|vignette|upright=1.5|Accès mémoire simultanés.]]
Évidemment, quand la mémoire FIFO est pleine, le pipeline est alors totalement bloqué. Le rasteriser est prévenu que l'unité de texture ne peut pas accepter de nouvelle lecture de texture. En pratique, la FIFO est généralement d'une taille respectable et permet de mettre en attente beaucoup de demandes de lecture de texture. Il faut de plus noter qu'il y a une FIFO par processeur de ''shader'' sur les cartes graphiques modernes. Quand elle est pleine, le processeur cesse d'exécuter de nouveaux accès mémoire, mais peut continuer à exécuter des ''shaders'' dans les autres unités de calcul, pas besoin de bloquer complétement le pipeline.
===L'intégration du cache de textures===
Il faut noter que les unités de texture incorporent aussi un cache de texture, voire plusieurs. L'intégration des caches de texture avec la mémoire FIFO précédente est quelque peu compliqué, car il faut garantir que les lectures de texture se fassent dans le bon ordre. On ne peut pas exécuter une lecture dans le cache alors que des lectures précédentes sont en attente de lecture en mémoire vidéo. Et cela pose un gros problème : une lecture dans le cache de texture prend quelques dizaines de cycles d'horloge, alors qu'une lecture en mémoire vidéo en prend facilement 400 à 800 cycles, parfois plus. Et cela fait que l'ordre des accès mémoire peut s'inverser.
Prenons par exemple un accès au cache précédé et suivi par deux accès en mémoire vidéo. Le premier démarre au cycle 1, et se termine au cycle numéro 400. L'accès au cache commence au cycle 2 et se termine 20 cycles après, au cycle numéro 22. En clair, la lecture dans le cache s'est terminée avant l'accès mémoire qui le précède. Les textures ne sont donc plus lues dans l'ordre. Et il faut trouver une solution pour éviter cela.
La solution est de retarder les lectures dans le cache tant que tous les accès précédents ne sont pas terminés. Mais pour retarder les lectures en question, il faut d'abord savoir si la lecture atterrit dans le cache ou non, ce qui demande d'accéder au cache. On fait face à un dilemme : on veut retarder les accès au cache, mais les différencier des lectures déclenchant des accès mémoire demande d'accéder au cache en premier lieu. La solution est décrite dans l'article "Prefetching in a Texture Cache Architecture" par Igehy et ses collègues. Elle se base sur deux idées combinées ensemble.
La première idée est de séparer l'accès au cache en deux : une étape qui vérifie si les texels à lire sont dans le cache, et une étape qui accède aux données dans le cache lui-même. Un cache de texture est donc composé de deux circuits principaux. Le premier vérifie la présence des texels dans le cache. Il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Ensuite, en plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
La seconde idée est de retarder l'accès au cache entre les deux phases. La première étape d'un accès mémoire vérifie si la donnée est dans le cache ou non. Puis, on retarde la lecture des données, pour attendre que toutes les lectures précédentes soient terminées. Et enfin, troisième étape : la lecture des texels dans la mémoire cache proprement dite. Les accès mémoire passant par la mémoire vidéo se font de la même manière, à une différence près : la lecture dans le cache est remplacée par la lecture en mémoire vidéo. Tout démarre avec une demande à l'unité de tags, qui vérifie si le texel est dans le cache ou non. Puis on retarde l'accès tant que la mémoire vidéo est occupée, puis on effectue la lecture en mémoire vidéo.
Si ce n'est pas le cas, l'accès mémoire est envoyé à la mémoire vidéo comme précédemment, à savoir qu'il est mis en attente dans une mémoire FIFO, puis envoyé à la mémoire vidéo dès que celle-ci est libre. Mais en sortie de la mémoire, la donnée lue est envoyée dans le cache de texture, par dans l'unité de filtrage. Pour savoir où placer la donnée lue, l'unité de tag a réservé une ligne de cache précise, une adresse bien précise. L'adresse en question est disponible en lisant une autre mémoire FIFO, qui a mis en attente l'adresse en question, en attendant que l'accès mémoire se termine. La donnée est alors écrite dans le cache, puis lue par l'unité de filtrage de textures.
Pour une lecture dans le cache, le déroulement est similaire, mais sans le passage par la mémoire. La lecture fait une demande à l'unité de tag, et celle-ci répond que la donnée est bien dans le cache. Elle place alors l'adresse à lire dans la file d'attente. Une fois que les accès mémoire précédents sont terminés, l'adresse sort de la file d'attente et est envoyée à la mémoire de données. La lecture s'effectue, les texels sont envoyés à l'unité de filtrage de textures. La seule différence avec un ''phased cache'' normal est l'insertion de l'adresse à lire dans une FIFO qui vise à mettre en attente
[[File:Unité de texture avec un cache de texture.png|centre|vignette|upright=2.0|Unité de texture avec un cache de texture]]
Pour résumer, l'implémentation précédente garantit une exécution des lectures dans leur ordre d'arrivée. Et pour cela, elle retarde les lectures dans le cache tant que les lectures en mémoire précédentes ne sont pas terminées. L'accès au cache est plus rapide que l'accès en mémoire vidéo, mais le retard ajouté pour garantir l'ordre des lectures fait que le temps d'accès est très long.
==Le mip-mapping==
Le '''mip-mapping''' a pour but de légèrement améliorer les graphismes des objets lointains, tout en rendant les calculs de texture plus rapides. Formellement, le ''mip-mapping'' est une technique de filtrage de texture, mais nous l'abordons maintenant car elle est surtout liée au calcul d'adresse. Les unités de texture ont des circuits de filtrage de texture séparés des circuits de ''mip-mapping'' et de calcul d'adresse, d'où le fait que nous en parlons séparément.
Le problème résolu par le ''mip-mapping'' est le rendu des textures lointaines. Si une texture est plaquée sur un objet lointain, une bonne partie des détails est invisible pour l'utilisateur. Un pixel de l'écran est associé à plusieurs texels. Idéalement, la carte graphiques devrait lire tous ces texels et en faire une sorte de moyenne pondérée, pour calculer la couleur finale du pixel. Mais dans les faits, ce serait très gourmand et compliqué à implémenter en hardware. Une solution serait de ne garder que quelque texels, mais cela a tendance à créer des artefacts visuels (les textures affichées ont tendance à pixeliser). Le ''mip-mapping'' permet de réduire ces deux problèmes en même temps en précalculant cette moyenne pondérée pour des distances prédéfinies.
L'idée est d'utiliser plusieurs exemplaires d'une même texture à des résolutions différentes, chaque exemplaire étant adapté à une certaine distance. Par exemple, une texture sera stocké avec un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre de 128 * 128 et ainsi de suite jusqu’à un dernier exemplaire de 32 * 32 pixel. Chaque exemplaire correspond à un '''niveau de détail''', aussi appelé ''Level Of Detail'' (abrévié en LOD). La résolution utilisée diminue d'autant plus que l'objet est situé loin de la caméra. Les objets proches seront rendus avec la texture 512*512, ceux plus lointains seront rendus avec la texture de résolution 256*256, les textures 128*128 seront utilisées encore plus loin, et ainsi de suite jusqu'aux objets les plus lointains qui sont rendus avec la texture la plus petite de 32*32.
[[File:MipMap Example STS101.jpg|centre|vignette|upright=2|Exemples de mip-maps.]]
Le ''mip-mapping'' améliore grandement la qualité d'image. L'image d'exemple ci-dessous le montre assez bien.
[[File:Mipmapping example.png|centre|vignette|upright=2|Exemple de mipmapping.]]
Pour faciliter les calculs d'adresse, les LOD d'une même texture sont stockées les uns après les autres en mémoire (dans un tableau, comme diraient les programmeurs). Ainsi, pas besoin de se souvenir de la position en mémoire de chaque LOD : l'adresse de la texture de base, et quelques astuces arithmétiques suffisent. Prenons le cas où la texture de base a une taille L. le premier exemplaire est à l'adresse 0, le second niveau de détail est à l'adresse L, le troisième à l'adresse L + L/4, le suivant à l'adresse L + L/4 + L/16, et ainsi de suite. Le calcul d'adresse demande juste connaître le niveau de détails souhaité et l'adresse de base de la texture. Le niveau de détail voulu est calculé par les pixel shaders, en fonction de la coordonnée de profondeur du pixel à traiter.
Évidemment, cette technique consomme de la mémoire vidéo, vu que chaque texture est dupliquée en plusieurs exemplaires, en plusieurs LOD. Dans le détail, la technique du mip-mapping prend au maximum 33% de mémoire en plus (sans compression). Cela vient du fait qu'en prenant une texture dexu fois plus petite, elle prend 4 fois moins de mémoire : 2 fois moins de pixels en largeur, et 2 fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des LODs, texture de base comprise, prendra X + (X/4) + (X/16) + (X/256) + … Un petit calcul de limite donne 4/3 * X, soit 33% de plus.
===L'implémentation du mip-mapping dans l'unité de texture===
Le ''mip-mapping'' est lui aussi pris en charge par l'unité de calcul d'adresse, car cette technique change l'adresse de base de la texture. La gestion du ''mip-mapping'' est cependant assez complexe. Il est possible de laisser le pixel shader calculer quel niveau de détail utiliser, en fonction de la coordonnée de profondeur z du pixel à afficher. La carte graphique détermine alors automatiquement quelle texture lire, quel niveau de détail, automatiquement. Elle détermine aussi la bonne résolution pour la texture, qui est égal à la résolution de la texture de base, divisée par le niveau de détail. Pour résumer, le niveau de détail est envoyé aux unités de texture, qui s'occupent de calculer l'adresse de base et la résolution adéquates. Quelques calculs arithmétiques simples, donc, qui s'implémentent facilement avec quelques circuits.
Mais une autre méthode laisse la carte graphique déterminer le niveau de détail par elle-même. Dans ce cas, cela demande, outre les deux coordonnées de texture, de calculer la dérivée de ces deux coordonnées dans le sens horizontal et vertical, ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Les quatre dérivées sont les suivantes :
: <math>\frac{du}{dx}</math>, <math>\frac{dv}{dx}</math>, <math>\frac{du}{dy}</math>, <math>\frac{dv}{dy}</math>
Un bon moyen pour obtenir les dérivées demande de regrouper les pixels par groupes de 4 et de faire la différence entre leurs coordonnées de texture respectives. On peut calculer les deux dérivées horizontales en comparant les deux pixels sur la même ligne, et les deux dérivées verticales en comparant les deux pixels sur la même colonne. Mais cela demande de rastériser les pixels par groupes de 4, par ''quads''. Et c'est ce qui est fait sur les cartes graphiques actuelles, qui rastérisent des groupes de 4 pixels à la fois.
[[File:Texture sampler unit with mipmapping.png|centre|vignette|upright=2.0|Unité de texture avec mipmapping.]]
Malheureusement, le calcul exact utilisé pour le choix de la mip-map dépend du GPU considéré et peu de chose est connu quant à ces algorithmes. Il est possible d'inférer le comportement à partir d'observations, mais guère plus. Pour ceux qui veulent en savoir plus, je conseille la lecture de cet article de blog :
* [https://pema.dev/2025/05/09/mipmaps-too-much-detail/ Mipmap selection in too much detail]
==Le cube-mapping==
[[File:Cube mapped reflection example 2.JPG|vignette|Exemple de reflets environnementaux.]]
L''''environnement-mapping''' est une technique de calcul de divers effets graphiques liés à l'environnement, notamment des réflexions. L'idée est de plaquer une texture pré-calculée pour simuler l'effet de l'environnement sur une surface ou un objet 3D. Il en existe plusieurs versions différentes, mais la seule utilisée de nos jours est le ''cube-mapping'', où la texture de l'environnement est plaquée sur un cube, d'où son nom. Le cube en question est utilisé différemment suivant ce que l'on cherche à faire avec le ''cube-mapping''. Les deux utilisations principales sont le rendu du ciel et des décors, et les réflexions sur la surface des objets. Dans les deux cas, l'idée est de précalculer ce que l'on voit du point de vue de la caméra. On place la caméra dans la scène 3D, on place un cube centré sur la caméra, le cube est texturé avec ce que l'on voit de l'environnement depuis la caméra/l'objet de son point de vue.
[[File:Panorama cube map.png|centre|vignette|upright=2|L'illustration montre en premier lieu une ''cubemap'' avec les six faces mises en évidence, puis quel environnement 3D elle permet de simuler, le troisième illustration montrant comment la ''cubemap'' est utilisée pour simuler l'environnement.]]
Le rendu du ciel et des décors lointains dans les jeux vidéo se base sur des '''''skybox''''', à savoir un cube centré sur la caméra, sur lequel on ajoute des textures de ciel ou de décors lointains. Le cube est recouvert par une texture, qui correspond à ce que l'on voit quand on dirige le regard de la caméra vers cette face. Contrairement à ce qu'on pourrait croire, la skybox n'est pas les limites de la scène 3D, les limites du niveau d'un jeu vidéo ou quoique ce soit d'autre de lié à la physique de la scène 3D. La skybox est centrée sur la caméra, elle suit la caméra dans son mouvement. Centrer la skybox sur la caméra permet de simuler des décors très lointains, suffisamment lointain pour qu'on n'ait pas l'illusion de s'en rapprocher en se déplaçant dans la map. De plus, cela évite d'avoir à faire trop de calculs à chaque fois que l'on bouge la caméra. La texture plaquée sur le cube est une texture unique, elle-même découpée en six sous-textures, une par face du cube.
[[File:Skybox example.png|centre|vignette|upright=2|Exemple de Skybox.]]
[[File:Cube mapped reflection example.jpg|vignette|Réflexions calculées par une ''cubemap''.]]
Le ''cube-mapping'' est aussi utilisé pour des reflets. L'idée est de simuler les reflets en plaquant une texture pré-calculée sur l'objet réflecteur. La texture pré-calculée est un dessin de l'environnement qui se reflète sur l'objet, un dessin du reflet à afficher. En la plaquant la texture sur l'objet, on simule ainsi des reflets de l'environnement, mais on ne peut pas calculer d'autres reflets comme les reflets objets mobiles comme les personnages. Et il se trouve que la texture pré-calculée est une ''cubemap''. Pour les environnements ouverts, c'est la ''skybox'' qui est utilisée, ce qui permet de simuler les reflets dans les flaques d'eau ou dans des lacs/océans/autres. Pour les environnements intérieurs, c'est une cubemap spécifique qui utilisée. Par exemple, pour l'intérieur d'une maison, on a une ''cubemap'' par pièce de la maison. Les reflets se calculent en précisant quelle ''cubemap'' appliquer sur l'objet en fonction de la direction du regard.
[[File:Cube map level.png|centre|vignette|Cube map de l'intérieur d'une pièce d'un niveau de jeux vidéo.]]
Toujours est-il que les textures utilisées pour le ''cubemmapping'', appelées des ''cubemaps'', sont en réalité la concaténation de six textures différentes. En mémoire vidéo, la ''cubemap'' est stockée comme six textures les unes à la suite des autres. Lors du rendu, on doit préciser quelle face du cube utiliser, ce qui fait 6 possibilités. On a le même problème qu'avec les niveaux de détail, sauf que ce sont les faces d'une ''cubemap'' qui remplacent les textures de niveaux de détails. L'accès en mémoire doit donc préciser quelle portion de la ''cubemap'' il faut accéder. Et l'accès mémoire se complexifie donc. Surtout que l'accès en question varie beaucoup suivant l'API graphique utilisée, et donc suivant la carte graphique.
Le support des ''cubemaps'' dépend de l'API 3D, et surtout de si elle date ou si elle est récente :
* Les API 3D très anciennes ne gérent pas nativement les ''cubemaps'', qui doivent être émulées en logiciel en utilisant six textures différentes. Le pixel shader décide donc quelle ''cubemap'' utiliser, avec quelques calculs sur la direction du regard.
* Les API 3D récentes gèrent nativement les ''cubemaps''. Pour les versions les plus vielles de ces API, les six faces sont numérotées et l'accès à une ''cubemap'' précise quel face utiliser en donnant son numéro. La carte graphique choisit alors automatiquement la bonne texture. Mais cela demande de laisser le calcul de la bonne face au pixel shader.
* Dans les API 3D modenres, les ''cubemap'' sont gérées comme des textures en trois dimensions, adressées avec trois coordonnées u,v,w. La carte graphique utilise ces trois coordonnées de manière à en déduire quelle est la face pertinente, mais aussi les coordonnées u,v dans la texture de la face.
==Le filtrage de textures==
Plaquer des textures sans autre forme de procès ne suffit pas à garantir des graphismes d'une qualité époustouflante. La raison est que les sommets et les texels ne tombent pas tout pile sur un pixel de l'écran : le sommet associé au texel peut être un petit peu trop en haut, ou trop à gauche, etc. Une explication plus concrète fait intervenir les coordonnées de texture. Souvenez-vous que lorsque l'on traduit une coordonnée de texture u,v en coordonnées x,y, on obtient un résultat qui ne tombe pas forcément juste. Souvent, le résultat a une partie fractionnaire. Si celle-ci est non-nulle, cela signifie que le texel/sommet n'est pas situé exactement sur le pixel voulu et que celui-ci est situé à une certaine distance. Concrètement, le pixel tombe entre quatre texels, comme indiqué ci-dessous.
[[File:Filtrage texture.png|centre|vignette|upright=2.0|Position du pixel par rapport aux texels.]]
Pour résoudre ce problème, on doit utiliser différentes techniques d'interpolation, aussi appelées techniques de '''filtrage de texture''', qui visent à calculer la couleur du pixel final en fonction des texels qui l'entourent. Il existe de nombreux types de filtrage de textures, qu'il s'agisse du filtrage linéaire, bilinéaire, trilinéaire, anisotropique et bien d'autres.
Tous ont besoin d'avoir certaines informations qui sont généralement fournies par les circuits de calcul d'adresse. La première est clairement la partie fractionnaire des coordonnées x,y. La seconde est la dérivée de ces deux coordonnées dans le sens horizontal et vertical., ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Toujours est-il que le filtrage de texture est une opération assez lourde, qui demande beaucoup de calculs arithmétiques. On pourrait en théorie le faire dans les pixels shaders, mais le cout en performance serait absolument insoutenable. Aussi, les cartes graphiques intègrent toutes un circuit dédié au filtrage de texture, le ''texture sampler''. Même les plus anciennes cartes graphiques incorporent une unité de filtrage de texture, ce qui nous montre à quel point cette opération est importante.
[[File:Texture unit.png|centre|vignette|upright=2.0|Unité de texture.]]
On peut configurer la carte graphique de manière à ce qu'elle fasse soit du filtrage bilinéaire, soit du filtrage trilinéaire, on peut configurer le niveau de filtrage anisotropique, etc. Cela peut se faire dans les options de la carte graphique, mais cela peut aussi être géré par l'application. La majorité des jeux vidéos permettent de régler cela dans les options. Ces réglages ne concernent pas la texture elle-même, mais plutôt la manière dont l'unité de texture doit fonctionner. Ces réglages sur l''''état de l'unité de texture''' sont mémorisés quelque part, soit dans l'unité de texture elle-même, soit fournies avec la ressource de texture elle-même, tout dépend de la carte graphique. Certaines cartes graphiques mémorisent ces réglages dans les unités de texture ou dans le processeur de commande, et tout changement demande alors de réinitialiser l'état des unités de texture, ce qui prend un peu de temps. D'autres placent ces réglages dans les ressources de texture elles-mêmes, ce qui rend les modifications de configuration plus rapides, mais demande plus de circuits. D'autres cartes graphiques mélangent les deux options, certains réglages étant globaux, d'autres transmis avec la texture. Bref, difficile de faire des généralités, tout dépend du matériel et le pilote de la carte graphique cache tout cela sous le tapis.
Maintenant que cela est dit, voyons quelles sont les différentes méthodes de filtrage de texture et comment la carte graphique fait pour les calculer.
===Le filtrage au plus proche===
La méthode de filtrage la plus simple consiste à colorier avec le texel le plus proche. Cela revient tout simplement à ne pas tenir compte de la partie fractionnaire des coordonnées x,y, ce qui est très simple à implémenter en matériel. C'est ce que l'on appelle le '''filtrage au plus proche''', aussi appelé ''nearest filtering''.
Autant être franc, le résultat est assez pixelisé et peu agréable à l’œil. Par contre, le résultat est très rapide à calculer, vu qu'il ne demande aucun calcul à proprement parler. Elle ne fait pas appel à la parti fractionnaire des coordonnées entières de texture, ni aux dérivées de ces coordonnées. On peut combiner cette technique avec le mip-mapping, ce qui donne un résultat bien meilleur, bien que loin d'être satisfaisant. Au passage, toutes les techniques de filtrage de texture peuvent se combiner avec du mip-mapping, certaines ne pouvant pas faire sans.
[[File:Interpolation-nearest.svg|centre|vignette|Filtrage de texture au plus proche.]]
===Le filtrage linéaire===
Le filtrage le plus simple est le '''filtrage linéaire'''. Il effectue une interpolation linéaire entre deux mip-maps, deux niveaux de détails. Pour comprendre l'idée, nous allons prendre une situation très simple, avec une texture carrée de 512 texels de côté. Le mip-mapping crée plusieurs textures : une de 256 texels de côté, une de 128 texels, une de 64, etc. Maintenant, la texture est sur un objet à une certaine distance de l'écran, vu de face. Le résultat est qu'elle correspond à l'écran à un carré de 300 pixels de côté (pas d'erreur : pixels, pas texels). Dans ce cas, la texture se trouve entre deux mip-maps : celle de 512 pixels de côté, celle de 256. Laquelle choisir ? Le filtrage au plus proche prend la texture de 512 pixels de côté. Le filtrage linéaire lui, fait autrement.
Vu que la texture est entre deux mip-maps, l'idée est de prendre le texel au plus proche dans chaque texture et de faire une sorte de moyenne appelée l'interpolation linéaire. L'interpolation par du principe que la couleur varie entre les deux texels en suivant une fonction affine, illustrée ci-dessous. Ce ne serait évidemment pas le cas dans le monde réel, mais on supposer cela donne une bonne approximation de ce à quoi ressemblerait une texture à plus haute résolution. On peut alors calculer la couleur du pixel par une simple moyenne pondérée par la distance. Le résultat est que les transitions entre deux niveaux de détails sont plus lisses, moins abruptes.
[[File:Lin interp -é.png|centre|vignette|upright=2.0|Interpolation linéaire.]]
===Le filtrage bilinéaire===
Le filtrage bilinéaire effectue une sorte de moyenne pondérée des quatre texels les plus proches du pixel à afficher. Pour cela, rappelez-vous ce qui a été dit plus haut : les coordonnées x,y d'un pixel ont une partie entière et une partie fractionnaire. Le filtrage au plus proche élimine les parties fractionnaires, ce qui donne une coordonnée x,y. Avec le filtrage bilinéaire, on prend les texels de coordonnées (x,y) ; (x+1,y) ; (x,y+1) ; (x+1,y+1), le pixel étant entre ces 4 texels.
Mais le filtrage ne fait pas qu'une simple moyenne, il prend en compte les parties fractionnaires pour faire la moyenne. En effet, le pixel n'est pas au milieu du carré de texel, il est quelque part mais est souvent plus proche d'un texel que des autres. Et il faut donc pondérer la moyenne par les distances aux 4 texels. Pour cela, la moyenne est calculée à partir d'interpolations linéaires. Avec 4 pixels, nous allons devoir calculer la couleur de deux points intermédiaires. La couleur de ces deux points se calcule par interpolation linéaire, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.
[[File:Bilin3.png|centre|vignette|upright=2|Filtrage bilinéaire de texture.]]
Le circuit qui permet de faire l'interpolation bilinéaire est particulièrement simple. On trouve un circuit de chaque pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu, et un pour la transparence. Chacun de ces circuit est composé de sous-circuits chargés d'effectuer une interpolation linéaire, reliés comme suit.
[[File:Texture sampler unit.png|centre|vignette|Unité de filtrage bilinéaire.]]
Vous noterez que le filtrage bilinéaire accède à 4 pixels en même temps. Fort heureusement, les textures sont stockées de manière à ce qu'on puisse charger les 4 pixels en une fois, comme on l'a vu plus haut. Le filtrage bilinéaire a de fortes chances que les 4 pixels filtrés soient dans la même ''tile'', la seule exception étant quand ils sont tout juste sur le bord d'une ''tile''.
: La console de jeu Nintendo 64 n'utilise que trois pixels au lieu de quatre dans son interpolation bilinéaire, qui en devient une interpolation quasi-bilinéaire. La raison derrière ce choix est une question de performances, comme beaucoup de décisions de ce genre. Le résultat est un rendu imparfait de certaines textures.
===Le filtrage trilinéaire===
Avec le filtrage bilinéaire, des discontinuités apparaissent sur certaines surfaces. Par exemple, pensez à une texture de sol : elle est appliquée plusieurs fois sur toute la surface du sol. A une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512*512 à 256*256, ce qui est visible pour un joueur attentif. De telles transitions sont lissées grâce au filtrage linéaire, il n'y a plus qu'à le combiner avec le filtrage bilinéaire. Rien d’incompatible : le premier filtre l'intérieur d'une mip-map, le second combine deux mip-maps.
Le filtrage trilinéaire prend les deux mip-maps les plus proches, fait un filtrage bilinéaire avec chacune, puis fait une « une moyenne » pondérée entre les deux résultats. Le circuit de filtrage trilinéaire existe en plusieurs versions. La plus simple, illustrée ci-dessous, effectue deux filtrages bilinéaires en parallèle, dans deux circuits séparés, puis combine leurs résultats avec un circuit d'interpolation linéaire. Mais ce circuit nécessite de charger 8 texels simultanément. Qui plus est, ces 8 texels ne sont pas consécutifs en mémoire, car ils sont dans deux niveaux de détails/mip-maps différents.
[[File:Parallel trilinear filtering.png|centre|vignette|upright=2.0|Unité de filtrage trilinéaire parallèle.]]
Vu qu'on lit des texels dans deux mip-maps, les texels sont lus en deux fois : 4 texels provenant de la première mip-map, suivis par les 4 texels de l'autre mip-map. Les 4 premiers texels doivent donc être mis en attente dans des registres, en attendant que les 4 autres arrivent. Une amélioration du circuit précédent gère cela en ajoutant des registres. Il lit les 4 premiers texels, les filtre avec une interpolation bilinéaire, et mémorise le résultat dans un registre. Puis, il lit les 4 autres texels, les filtre, et met le résultat dans un second registre. A ce moment là, un circuit d'interpolation linéaire finit le travail. On économise donc un circuit d'interpolation bilinéaire, sans que les performances soient trop impactées.
[[File:Filtrage trilineaire.png|centre|vignette|upright=1.0|Unité de filtrage trilineaire série.]]
Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le temps où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. À chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients, et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.
===Le filtrage anisotrope===
D'autres artefacts peuvent survenir lors de l'application d'une texture, la perspective pouvant déformer les textures et entraîner l'apparition de flou. La raison à cela est que les techniques de filtrage de texture précédentes partent du principe que la texture est vue de face. Prenez une texture carrée, par exemple. Vue de face, elle ressemble à un carré sur l'écran. Mais tournez la caméra, de manière à voir la texture de biais, avec un angle, et vous verrez que la forme de la texture sur l'écran est un trapèze, pas un carré. Cette déformation liée à la perspective n'est pas prise en compte par les méthodes de filtrage de texture précédentes. Pour le dire autrement, les techniques de filtrage précédentes partent du principe que les 4 texels qui entourent un pixel forment un carré, ce qui est vrai si la texture est vue de face, sans angle, mais ne l'est pas si la texture n'est pas perpendiculaire à l'axe de la caméra. Du point de vue de la caméra, les 4 texels forment un trapèze d'autant moins proche d'un carré que l'angle est grand.
Pour corriger cela, les chercheurs ont inventé le '''filtrage anisotrope'''. En fait, je devrais plutôt dire : LES filtrages anisotropes. Il en existe un grand nombre, dont certains ne sont pas utilisés dans les cartes graphiques actuelles, soit car ils trop gourmand en accès mémoires et en calculs pour être efficaces, soit car ils ne sont pas pratiques à mettre en œuvre. Il est très difficile de savoir quelles sont les techniques de filtrage de texture utilisées par les cartes graphiques, qu'elles soient récentes ou anciennes. Beaucoup de ces technologies sont brevetées ou gardées secrètes, et il faudrait vraiment creuser les brevets déposés par les fabricants de GPU pour en savoir plus. Les algorithmes en question seraient de plus difficiles à comprendre, les méthodes mathématiques cachées derrière ces méthodes de filtrage n'étant pas des plus simple.
[[File:Anisotropic filtering en.png|centre|vignette|upright=2|Exemple de filtrage anisotrope.]]
==La compression de textures==
Les textures les plus grosses peuvent aller jusqu'au mébioctet, ce qui est beaucoup. Pour limiter la casse, les textures sont compressées. La '''compression de texture''' réduit la taille des textures, ce qui peut se faire avec ou sans perte de qualité. Elle entraîne souvent une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande. Mais il s'agit là d'une technique très simple, beaucoup plus simple que les techniques que nous allons voir dans cette section. Nous allons voir quelque algorithmes de compression de textures de complexité intermédiaire, mais n'allons pas voir l'état de l'art. Il existe des formats de texture plus récents que ceux qui nous allons aborder, comme l{{'}}''Ericsson Texture Compression'' ou l{{'}}''Adaptive Scalable Texture Compression'', plus complexes et plus efficaces.
Notons que les textures sont compressées dans les fichiers du jeu, mais aussi en mémoire vidéo. Les textures sont décompressées lors de la lecture. Pour cela, la carte graphique contient alors un circuit, capable de décompresser les textures lorsqu'on les lit en mémoire vidéo. Les cartes graphiques supportent un grand nombre de formats de textures, au niveau du circuit de décompression. Du fait que les textures sont décompressées à la volée, les techniques de compression utilisées sont assez particulières. La carte graphique ne peut pas décompresser une texture entière avant de pouvoir l'utiliser dans un ''pixel shader''. A la place, on doit pouvoir lire un morceau de texture, et le décompresser à la volée. On ne peut utiliser les méthodes de compression du JPEG, ou d'autres formats de compression d'image. Ces dernières ne permettent pas de décompresser une image morceau par morceau.
Pour permettre une décompression/compression à la volée, les textures sont des textures tilées, généralement découpées en tiles de 4 * 4 texels. Les ''tiles'' sont compressées indépendamment les unes des autres. Et surtout, avec ou sans compression, la position des tiles en mémoire ne change pas. On trouve toujours une tile tous les T octets, peu importe que la tile soit compressée ou non. Par contre, une tile compressée n'occupera pas T octets, mais moins, là où une tile compressée occupera la totalité des T octets. En clair, compresser une tile fait qu'il y a des vides entre deux tiles dans al mémoire vidéo, mais ne change rien à leur place en mémoire vidéo qui est prédéterminée, peu importe que la texture soit compressée ou non. L'intérêt de la compression de textures n'est pas de réduire la taille de la texture en mémoire vidéo, mais de réduire la quantité de données à lire/écrire en mémoire vidéo. Au lieu de lire T octets pour une tile non-compressée, on pourra en lire moins.
===La palette indicée et la technique de ''Vector quantization''===
La technique de compression des textures la plus simple est celle de la '''palette indicée''', que l'on a entraperçue dans le chapitre sur les cartes d'affichage. La technique de '''''vector quantization''''' peut être vue comme une amélioration de la palette, qui travaille non pas sur des texels, mais sur des ''tiles''. À l'intérieur de la carte graphique, on trouve une table qui stocke toutes les ''tiles'' possibles. Chaque ''tile'' se voit attribuer un numéro, et la texture sera composé d'une suite de ces numéros. Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l’embarqué utilisent ce genre de compression.
===Les algorithmes de ''Block Truncation coding''===
La première technique de compression élaborée est celle du '''''Block Truncation Coding''''', qui ne marche que pour les images en niveaux de gris. Le BTC ne mémorise que deux niveaux de gris par ''tile'', que nous appellerons couleur 1 et couleur 2, les deux niveaux de gris n'étant pas le même d'une ''tile'' à l'autre. Chaque pixel d'une ''tile'' est obligatoirement colorié avec un de ces niveaux de gris. Pour chaque pixel d'une ''tile'', on mémorise sa couleur avec un bit : 0 pour couleur 1, et 1 pour couleur 2. Chaque ''tile'' est donc codée par deux entiers, qui codent chacun un niveau de gris, et une suite de bits pour les pixels proprement dit. Le circuit de décompression est alors vraiment très simple, comme illustré ci-dessous.
[[File:Block Truncation coding.jpg|centre|vignette|upright=2.0|Block Truncation coding.]]
La technique du BTC peut être appliquée non pas du des niveaux de gris, mais pour chaque composante Rouge, Vert et Bleu. Dans ces conditions, chaque ''tile'' est séparée en trois sous-''tiles'' : un sous-bloc pour la composante verte, un autre pour le rouge, et un dernier pour le bleu. Cela prend donc trois fois plus de place en mémoire que le BTC pur, mais cela permet de gérer les images couleur.
===Le format de compression S3TC / DXTC===
L'algorithme de '''Color Cell Compression''', ou CCC, améliore le BTC pour qu'il gère des couleurs autre que des niveaux de gris. Ce CCC remplace les deux niveaux de gris par deux couleurs. Une ''tile'' est donc codée avec un entier 32 bits par couleur, et une suite de bits pour les pixels. Le circuit de décompression est identique à celui utilisé pour le BTC.
[[File:Color Cell Compression.jpg|centre|vignette|Color Cell Compression.]]
[[File:Dxt1-memory-layout.png|vignette|Dxt1 et ''color cell compression''.]]
Le format de compression de texture utilisé de base par Direct X, le DXTC, est une version amliorée de l'algorithme précédent. Il est décliné en plusieurs versions : DXTC1, DXTC2, etc. La première version du DXTC est une sorte d'amélioration du CCC : il ajoute une gestion minimale de transparence, et découpe la texture à compresser en ''tiles'' de 4 pixels de côté. La différence, c'est que la couleur finale d'un texel est un mélange des deux couleurs attribuée au bloc. Pour indiquer comment faire ce mélange, on trouve deux bits de contrôle par texel.
Si jamais la couleur 1 < couleur2, ces deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (2 * Couleur1 + Couleur2) / 3
* 11 = (Couleur1 + 2 * Couleur2) / 3
Sinon, les deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (Couleur1 + Couleur2) / 2
* 11 = Transparent
[[File:DXTC.jpg|centre|vignette|DXTC.]]
Le circuit de décompression du DXTC ressemble alors à ceci :
[[File:Circuit de décompression du DXTC.jpg|centre|vignette|upright=2.0|Circuit de décompression du DXTC.]]
===Les format DXTC 2, 3, 4 et 5 : l'ajout de la transparence===
Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3, lui-même replacé par le DXT4 et par le DXT5. Dans le DXT3, la transparence fait son apparition. Pour cela, on ajoute 64 bits par ''tile'' pour stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1.
[[File:Dxt23-memory-layout.png|centre|vignette|Dxt 2 et 3.]]
Dans le DXT4 et le DXT5, la méthode utilisée pour compresser les couleurs l'est aussi pour les valeurs de transparence. L'information de transparence est stockée par un en-tête contenant deux valeurs de transparence, le tout suivi d'une matrice qui attribue trois bits à chaque texel. En fonction de la valeur des trois bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1.
[[File:Dxt45-memory-layout.png|centre|vignette|Dxt 4 et 5.]]
===Le format de compression PVRTC===
Passons maintenant à un format de compression de texture un peu moins connu, mais pourtant omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle travaille surtout dans les cartes graphiques embarquées. Ses cartes se trouvent notamment dans l'ipad, l'iPhone, et bien d'autres smartphones actuels.
Avec le PVRTC, les textures sont encore une fois découpées en ''tiles'' de 4 texels par 4, mais la ressemblance avec le DXTC s’arrête là. Chacque ''tile'' est codée avec :
* une couleur codée sur 16 bits ;
* une couleur codée sur 15 bits ;
* 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
* et un bit de modulation, qui permet de configurer l’interprétation des bits de mélange.
Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4*4.
==Annexe : le ''normal-mapping'' hardware==
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, parlons un peu du ''normal mapping'' et de son implémentation dans les unités de texture. Pour rappel, 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. Et pour comprendre quel est le rapport avec les textures, nous allons devoir faire quelques rappels sur l'éclairage par pixel.
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
L'éclairage se fait en utilisant de nombreux calculs, qu'on a détaillé dans le chapitre sur les bases du rendu 3D. Ceux-ci utilisent la normale d'un sommet, à savoir un vecteur orienté à la verticale de la surface. En théorie, il y a une normale par sommet, ce qui fait que les calculs d'éclairage doivent se faire au niveau géométrique, avant la rastérisation. Mais pour obtenir un éclairage de meilleure qualité, il y a des techniques qui calcule l'éclairage d'une scène 3D pixel par pixel.
Une première technique d'éclairage par pixel interpole les normales lors de l'étape de rastérisation. On obtient alors un éclairage de Phong. Une autre solution est celle du ''normal mapping''. Le '''''normal mapping''''' précalcule les normales d'une surface dans une texture, appelée la ''normal map''. L'éclairage est alors réalisé par un pixel shader, qui lit les normales depuis cette texture et fait les calculs d'éclairage avec.
===L'usage de textures non-compressées pour les ''normal maps''===
La ''normal map'' est une texture, donc. Mais ce n'est pas une texture comme une autre. Elle mémorise un vecteur pour chaque texel, pas une couleur. Il est possible d'utiliser les formats de textures non-compressés, même si cela ne donne pas de bons résultats. Après tout, un vecteur est codé par trois coordonnées x,y,z, il est possible de coder chacune avec un octet et de packager cela dans une texture RGB classique. La coordonnée x va dans la composante Rouge, la coordonnée Y dans la composante Bleu, et la coordonnée z dans la composante z.
Une première optimisation est de ne pas mémoriser les trois coordonnées, mais seulement deux d'entre elles et de calculer la troisième. En effet, les normales sont des vecteurs ''normalisés'', c'est à dire que leur longueur vaut 1, par construction. Vu que la taille est connue à l'avance, on peut en déduire la coordonnée z à partir des coordonnées x et y, avec l'usage du théorème de Pythagore. Par définition, <math>z^2 = 1 - x^2 - y^2</math>. Le calcul peut être fait dans le ''pixel shader'' sans problème.
: Pour être précis, il faut que les normales soient définies d'une certaine manière pour que ça marche. Les normales ne sont pas définies dans le même système de coordonnées que le modèle 3D, mais dans un autre système appelé l'''espace tangent''.
Notons que l'accès à la ''normal map'' se fait comme pour n'importe quelle texture : les texels sont lus en mémoire vidéo, puis le filtrage de texture est appliqué, et enfin le tout est envoyé au ''pixel shader''. En théorie, le filtrage effectue une sorte d'interpolation des normales automatique, ce qui permet de trouver la normale à un pixel précis, même s'il n'est pas sur un texel.
Les solutions précédentes demandent d'utiliser des textures non-compressées, qui utilisent beaucoup de mémoire vidéo. Utiliser des textures compressées pourrait sembler résoudre le problème. Mais l'usage de textures compressées marche très mal pour les ''normal maps'', elles ne permettent pas d'obtenir une qualité ou une compression suffisante. Aussi, des formats de texture dédiés aux ''normal maps'' ont été inventés.
===Le format de texture 3Dc===
Le problème est que le rendu final n'est pas très beau. Et les opérations de filtrage ne donnent pas de très bons résultats. Pour remédier à ces problèmes, des formats de texture spécialisés pour les ''normal map'' ont été inventés. Le premier d'entre eux est le '''3Dc''', aussi appelé BC5 pour ''Block Compression 5''. Il utilise un octet par texel, au lieu de trois avec une texture RGB normale non-compressée. Et surtout : il est géré en matériel, directement dans l'unité de texture, qui peut décompresser les textures 3Dc et les filtrer !
Avec le 3Dc, seules coordonnées sont mémorisées, la troisième est calculée, comme dit plus haut. Pour le reste, la compression de la ''normal map'' ressemble un peu à celle des textures. La ''normal map'' est découpée en blocs de 4 par 4 texels de côté, chacune étant encodée en tenant compte de certaines redondances. Dans un bloc, les coordonnées x sont dans un certain intervalle, allant de <math>x_{min}</math> à <math>x_{max}</math>, idem pour la coordonnée y qui est dans l'intervalle <math>y_{min}</math>, <math>y_{max}</math>. L'intervalle <math>x_{min}</math> à <math>x_{max}</math> est coupé en 8 parts égales, ce qui fait 8 valeurs possibles dans cet intervalle, chacune correspondant à une valeur x possible pour une normale du bloc. La normale est donc encodée en précisant quelle valeur est la bonne, en utilisant un indice de 3 bits pour cela. La valeur exacte de la coordonnée x se calcule à partir de l'indice comme suit :
: <math>x = x_{min} + \left( \frac{x_{min} - x_{min}}{8} \right) \times \text{indice} </math>
Pour encoder un bloc de 4 par 4 normales, il faut donc :
* Un octet pour chaque coordonnée <math>x_{min}</math>, <math>x_{max}</math>, <math>y_{min}</math>, <math>y_{max}</math>.
* 6 bits par normale : 3 pour encoder le décalage de la coordonnée x, 3 pour la coordonnée y.
Le tout permet d'encoder un bloc de 16 normales en seulement 128 bits, soit un octet par normale. Le ''pixel shader'' se débrouille pour décompresser un bloc. Il effectue notamment le calcul du dessus, pour retrouver la coordonnée x. Les calculs étant particulièrement simples, le cout en performance est très faible. Et ce d'autant plus qu'ils demandent juste de faire des additions et des multiplications entières, que l'unité scalaire peut faire en parallèle d'autres opérations plus gourmandes. Et le cout en calculs est de toute façon compensé par l'économie de mémoire et de bande passante lié à la compression : diviser la taille des données par trois, ca a un sacré impact !
==Annexe : les ''shadowmap'' hardware==
Les anciens GPU, notamment la Geforce FX, avaient des fonctionnalités spécifiques pour le calcul des ombres. Dans la plupart des jeux vidéos de l'époque, et même de maintenant, les ombres sont calculées avec la technique des ''shadowmap''. L'idée est assez simple sur le principe : un pixel est dans l'ombre quand il est invisible depuis une source de lumière.
L'idée est que le rendu est réalisé en plusieurs passes, avec une passe par source de lumière et une passe finale pour calculer l'image finale. Nous allons expliquer la technique avec une seule source de lumière, et allons utiliser l'exemple de la scène ci-dessous.
[[File:7fin.png|centre|vignette|Scène 3D d'exemple.]]
===La technique du ''shadowmapping''===
[[File:2shadowmap.png|vignette|Résultat de la première passe : ''shadowmap''..]]
La première passe rend l'image depuis le point de vue de la source de lumière. Cette première passe ne rend pas les couleurs de la scène, elle ne s'intéresse qu'à la profondeur des pixels. Le résultat est que l'image ne rend que le tampon de profondeur. Celui-ci est ensuite réutilisé comme texture pour la passe suivante. La texture en question est appelée la '''''shadownmap'''''.
La perspective utilisée, ainsi que le ''view frustrum'', dépend de la source de lumière. Pour une source de lumière qui émet un cône de lumière, le ''view frustrum'' de l'image rendue doit contenir tout le cone de lumière, et doit coller le plus possible à celui-ci. Pour une source directionnelle, comme le soleil, une perspective orthographique est utilisée.
La seconde passe rend l'image du point de vue de la caméra, pour rendre l'image finale. Elle rend l'image finale, qui est composée de pixels, chacun ayant une position à l'écran x,y, et une profondeur z. Les coordonnées sont transformées pour obtenir la position de ce pixel depuis le point de vue de la caméra. Une simple multiplication de matrice suffit, rien de bien compliqué, un shader peut le faire.
[[File:5failed.png|vignette|Résultat du test des comparaisons.]]
Après cette étape, on a alors les coordonnées x,y,z de ce pixel du point de vue de la caméra, et la ''shadowmap''. Il est alors possible d'accéder à la ''shadowmap'' au même endroit, à la même place que le pixel testé, aux mêmes coordonnées x,y. Si la profondeur du pixel est supérieure à celle de la shadowmap au même endroit, alors le pixel est situé derrière la surface visible, donc est dans l'ombre. Sinon, il n'est pas dans l'ombre. Le même procédé est répété sur chaque pixel de l'écran.
===Les optimisations hardware du ''shadowmapping''===
La technique des ''shadowmap'' demande donc de calculer une texture ''shadowmap'', puis de lire celle-ci et de faire des comparaisons de profondeur. Les GPU comme la Geforce FX intégraient du matériel dans les unités de texture pour faciliter ce travail. Les unités de texture pouvaient lire les ''shadowmap'', et faire la comparaison de profondeur toutes seules, elles avaient des circuits pour. Il suffisait de leur fournir le pixel à tester, ses coordonnées x,y,z, et l'adresse de la ''shadowmap''. Les unités de texture renvoyaient alors un résultat valant 0 ou 1 : 1 si le pixel est dans l'ombre, 0 sinon.
Elles pouvaient même effectuer du filtrage de texture sur les ''shadowmap''. Mais le filtrage était différent de celui utilisé sur les autres textures : moyenner des valeurs de profondeur ne marche pas bien. Elles utilisaient des techniques de filtrage différentes : elles faisaient les tests de comparaison, puis faisaient la moyenne des résultats. Ainsi, pour du filtrage bilinéaire, elles lisaient 4 texels dans la ''shadowmap'', puis faisaient 4 tests de comparaison, et moyennaient les 4 résultats.
==Annexe : les textures virtuelles==
Les '''textures virtuelles''' sont une optimisation des textures normales, qui visent à accélérer le rendu de terrains de grande taille. Imaginez par exemple un monde assez ouvert, comme un environnement en forêt ou en montagne, avec une grande distance de visibilité. Avec de tels terrains, le "sol" est recouvert par une texture de sol unique qui recouvre tout le terrain. Elle ne se répète pas, est de très grande taille, et peut parfois recouvrir toute la map ! Mais il n'y a pas assez de mémoire vidéo pour mémoriser la texture toute entière. La seule solution est la suivante : une partie de la texture est placée en mémoire vidéo, le reste est soit placé en mémoire RAM ou sur le disque dur.
Pour cela, le moteur de jeu utilise une optimisation ingénieuse, basée sur une observation assez basique : une bonne partie de la texture est visible, mais le reste est caché par des arbres, des habitations ou d'autres obstacles. Une optimisation possible de ne garder en mémoire vidéo que les portions visibles de la texture, pas les portions cachées. Une autre optimisation mélange textures virtuelles et ''mip-mapping''. L'idée est que pour les portions lointaines d'une texture, la texture utilisée est une ''mip-map'' de basse résolution. L'idée est alors de ne charger que la ''mip-map'' adéquate, pas les autres niveaux de détail. En clair, la texture de base n'est pas chargée en mémoire vidéo, mais la ''mip-map'' basse résolution l'est.
===Une texture à deux niveaux===
L'implémentation des textures virtuelles découpe les méga-textures en ''tiles'', en morceaux rectangulaires de taille modeste. En clair, le terrain est découpé en morceau rectangulaires/carrés. Seules les tiles nécessaires sont chargées en mémoire vidéo, pas les autres. Par exemple, les ''tiles'' non-visibles ne sont pas placées en mémoire vidéo, seules les ''tiles'' visibles le sont. De même, il y a une ''tile'' par niveau de mip-map : seul la tile correspondant le niveau adéquat est en mémoire vidéo, les autres niveaux de détail ne sont pas chargés. On peut faire une analogie avec la mémoire virtuelle, où les données sont découpées en pages, qui sont chargées en mémoire RAM à la demande, suivant les besoins, les données pouvant être swappées sur le disque dur si elles sont peu utilisées. Sauf qu'ici, il s'agit de textures qui sont découpées en pages chargées à la demande en mémoire vidéo, depuis la RAM système.
Une texture virtuelle est en réalité un système à deux niveaux : une liste de ''tiles'' et les ''tiles'' elles-mêmes. La liste de ''tiles'' est appelée un '''atlas de texture''', c'est un peu l'équivalent de la ''tilemap'' pour le rendu 2D. Rendre une texture demande de calculer quelle ''tile'' contient le texel à afficher, consulter la ''tile'' en question, puis récupérer le texel adéquat dans cette ''tile''. La ''tile'' est donc une texture, mais la texture à charger est choisie parmi un ensemble, qui est ici l'atlas de texture.
===L'implémentation : logicielle versus matérielle===
Les textures virtuelles ont été utilisées pour la première fois par les jeux Rage 1 et 2 d'IdSoftware, et quelques jeux ultérieurs comme DOOM 2016. IdSoftware les appelait des '''''mega-textures'''''. L'optimisation permettait des gains en performance assez impressionnants. Le jeu Rage 1 utilisait une texture carrée unique de 128k pixels de côté pour rendre le terrain. En théorie, une telle texture devrait prendre 64 giga-octets, mais le jeu tournait correctement avec 512 méga-octets de RAM, poussivement avec seulement 256 méga-octets de RAM.
De nos jours, les textures virtuelles sont supportées par beaucoup de jeux vidéos, les moteurs les plus courants gèrent de telles textures de manière logicielles. Mais quelques GPU récents supportent les textures virtuelles. Sur les GPU récents, l'atlas de texture est géré nativement par le matériel. Le GPU choisit quelle ''tile'', quelle texture choisir pour rendre le texel adéquat. Pour cela, le GPU calcule quelle ''tile'' charger, consulte l'atlas de texture, et lit la texture de ''tile'' adéquate.
Mais l'implémentation sur les GPU récents a de nombreuses limitations. La limitation la plus importante est que la taille des textures virtuelles ne peut pas dépasser la taille d'une texture normale, soit 32768 pixels de côté pour une texture carrée environ sur les GPU de 2020. De plus, le chargement d'une ''tile'' est très lent. En clair, dès qu'on veut changer de niveau de mip-map pour une tile, ou dès qu'une tile devient visible, le chargement de la tile peut facilement prendre plusieurs centaines de millisecondes. Le filtrage de texture est très complexe avec des textures virtuelles, ce qui fait que le filtrage de texture virtuelle est souvent soumis à des limitations que les textures normales n'ont pas, notamment pour le filtrage anisotropique.
{{NavChapitre | book=Les cartes graphiques
| prev=Le rasterizeur
| prevText=Le rasterizeur
| next=Les Render Output Target
| nextText=Les Render Output Target
}}{{autocat}}
ju941aac55k9ywsgyzdqymuxpkx2dam
763421
763420
2026-04-10T19:28:00Z
Mewtow
31375
/* Le cube-mapping */
763421
wikitext
text/x-wiki
[[File:Texture mapping.png|vignette|''Texture mapping'']]
Les '''textures''' sont des images que l'on va plaquer sur la surface d'un objet, du papier peint en quelque sorte. Les cartes graphiques supportent divers formats de textures, qui indiquent comment les pixels de l'image sont stockés en mémoire : RGB, RGBA, niveaux de gris, etc. Une texture est donc composée de "pixels", comme toute image numérique. Pour bien faire la différence entre les pixels d'une texture, et les pixels de l'écran, les pixels d'une texture sont couramment appelés des ''texels''.
==Le placage de textures inverse==
Pour rappel, plaquer une texture sur un objet consiste à attribuer un texel à chaque sommet, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture.
Dans les faits, on n'utilise pas de coordonnées entières de ce type. Les coordonnées de texture sont deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. Le nom donnée à cette technique de description des coordonnées de texture s'appelle l''''''UV Mapping'''''.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Les API 3D modernes gèrent des textures en trois dimensions, ce qui ajoute une troisième coordonnée de texture notée w. Dans ce qui va suivre, nous allons passer les textures en trois dimensions sous silence. Elles ne sont pas très utilisées, la quasi-totalité des jeux vidéo et applications 3D utilisant des textures en deux dimensions. Par contre, le matériel doit gérer les textures 3D, ce qui le rend plus complexe que prévu. Il faut ajouter quelques circuits pour, de quoi gérer la troisième coordonnée de texture, etc.
Lors de la rastérisation, chaque fragment se voit attribuer un sommet, et donc la coordonnée de texture qui va avec. Si un pixel est situé pile sur un sommet, la coordonnée de texture de ce sommet est attribuée au pixel. Si ce n'est pas le cas, la coordonnée de texture finale est interpolée à partir des coordonnées des trois sommets du triangle rastérisé. L'interpolation en question a lieu dans l'étape de rastérisation, comme nous l'avons vu dans le chapitre précédent. Le fait qu'il y ait une interpolation fait que les coordonnées du pixel gagent à être des nombres flottants. On pourrait faire une interpolation avec des coordonnées de texture entières, mais les arrondis et autres imprécisions de calcul donneraient un résultat graphiquement pas terrible, et empêcheraient d'utiliser les techniques de filtrage de texture que nous verrons dans ce chapitre.
À partir de ces coordonnées de texture, la carte graphique calcule l'adresse du texel qui correspond, et se charge de le lire. Et toute la magie a lieu dans ce calcul d'adresse, qui part de coordonnées de texture flottante, pour arriver à une adresse mémoire. Le calcul de l'adresse du texel se fait en plusieurs étapes, que nous allons voir ci-dessous. La première étape convertit les coordonnées flottantes en coordonnées entières, qui disent à quel ligne et colonne se trouve le texel voulu dans la texture. L'étape suivante transforme ces coordonnées x,y entières en adresse mémoire.
===La normalisation des coordonnées===
J'ai dit plus haut que les coordonnées de texture sont des coordonnées flottantes, comprises entre 0 et 1. Mais il faut savoir que les pixels shaders peuvent modifier celles-ci pour mettre en œuvre certains effets graphiques. Et le résultat peut alors se retrouver en-dehors de l'intervalle 0,1. C'est quelque chose de voulu et qui est traité par la carte graphique automatiquement, sans que ce soit une erreur. Au contraire, la manière dont la carte graphique traite cette situation permet d'implémenter des effets graphiques comme des textures en damier ou en miroir.
[[File:Clamp tile.jpg|vignette|Clamp tile]]
Il existe globalement trois méthodes très simples pour gérer cette situation, qui sont appelés des '''modes d'adressage de texture'''.
* La première méthode est de faire en sorte que le résultat sature. Si une coordonnée est inférieur à 0, alors on la remplace par un zéro. Si elle est supérieure à 1, on la ramène à 1. Avec cette méthode, tout se passe comme si les bords de la texture étaient étendus et remplissaient tout l'espace autour de la texture. Le tout est illustré ci-dessous. Ce mode d'accès aux textures est appelé le '''''clamp'''''.
* Une autre solution retire la partie entière de la coordonnée, elle coupe tout ce qui dépasse 1. Pour le dire autrement, elle calcule le résultat modulo 1 de la coordonnée. Le résultat est que tout se passe comme si la texture était répétée à l'infini et qu'elle pavait le plan.
* Une autre méthode remplit les coordonnées qui sortent de l’intervalle 0,1 avec une couleur préétablie, configurée par le programmeur.
===La conversion des coordonnées de textures flottantes en adresse mémoire===
Une fois la normalisation effectuée, les coordonnées de texture sont utilisées pour lire le texel voulu. Pour cela, les coordonnées de texte sont transformées en adresse mémoire, adresse qui pointe sur le texel ayant ces cordonnées. Pour cela, la première étape est de transformer les coordonnées flottantes u,v en coordonnées entières x,y qui pointent sur un texel. Pour cela, il suffit de multiplier les coordonnées flottantes u,v par la résolution de la texture accédée. Pour un écran de résolution <math>\text{height,width}</math>, le calcul est le suivant :
: <math>x = u \times \text{width}</math>
: <math>y = v \times \text{height}</math>
Le résultat est un nombre avec une partie entière et une partie fractionnaire. La partie entière des deux coordonnées donne la position x,y voulue, et la partie fractionnaire est conservée pour le filtrage de textures, mais passons cela sous silence pour le moment.
La seconde étape prend les coordonnées entières x,y et calcule l'adresse mémoire du texel. L'adresse dépend de la position de la texture en mémoire, précisément de son début, son premier texel, mais aussi de la position du texel par rapport au début de la texture. Et calculer cette position intra-texture dépend de la manière dont les texels sont stockés en mémoire.
====Les textures naïves====
Les programmeurs qui lisent ce cours s'attendent certainement à ce que la texture soit stockée en mémoire ligne par ligne, ou colonne par colonne. Cela veut dire que le premier pixel en partant d'en haut à gauche est stocké en premier, puis celui immédiatement à sa droite, puis celui encore à droite, et ainsi de suite. Une fois qu'on arrive à la fin d'une ligne, on passe à la ligne suivante, en-dessous. Cette organisation ligne par ligne s'appele l'organisation '''''row major order'''''. On peut faire pareil, mais colonne par colonne, ce qui donne le '''''column major order'''''.
[[File:Speicheranordnung Feld.svg|centre|vignette|upright=2|Row et column major order.]]
Maintenant, supposons que la texture commence à l'adresse <math>A_\text{texture}</math>, qui est l'adresse du premier texel. La texture a une résolution de <math>\text{width}</math> texels de large et <math>\text{height}</math> texels de haut. Par définition, les coordonnées X et Y des texels commencent à 0, ce qui fait que le pixel en haut à gauche a les coordonnées 0,0.
L'adresse du pixel se calcule comme suit :
: <math>A_\text{pixel} = A_\text{texture} + (\text{taille d'une ligne en octets} \times Y) + (\text{taille d'un texel en octets} \times X)</math>
La taille d'un pixel en mémoire est notée T. La taille d'une ligne en mémoire est de <math>width \times T</math>, par définition, vu qu'elle fait <math>width</math> texels. On a donc :
: <math>A_\text{pixel} = A_\text{texture} + (width \times T \times Y) + (T \times X)</math>
La formule se réécrit comme suit :
: <math>A_\text{pixel} = A_\text{texture} + T \times (width \times Y + X)</math>
Le calcul d'adresse est donc assez simple. Malheureusement, les textures ne sont pas stockées de cette manière en mémoire vidéo. En effet, elle se marie mal avec les opérations de filtrage de texture que nous allons voir dans ce qui suit. Le filtrage d'un texel dépend de ses voisins du dessus et du dessous. Le fait que la texture n'est pas forcément parcourue ligne par ligne fait que stocker une texture ligne par ligne n'est pas l'idéal.
De même, les textures sont déformées par la perspective. L'affichage de la texture ne se fait alors pas ligne par ligne, mais en parcourant la texture en diagonale, l'angle de la diagonale correspondant approximativement à l'angle que fait la verticale de la texture avec le regard. Vu qu'on ne connait pas à l'avance l'angle que fera la diagonale de parcours, on doit ruser.
====Les textures tilées====
Une première solution à ce problème est celle des '''textures tilées'''. Avec ces textures, l'image de la texture est découpée en ''tiles'', des rectangles ou en carrés de taille fixe, généralement des carrés de 4 pixels de côté. Les tiles ont une largeur et une longueur égales, afin de simplifier les calculs : on divise X et Y par le même nombre. De plus, leur largeur et leur longueur sont une puissance de deux, afin de simplifier les calculs d'adresse. Les ''tiles'' sont alors mémorisée les unes après les autres dans le fichier de la texture.
[[File:Texture tilée.png|centre|vignette|upright=2|Texture tilée]]
La formule de calcul d'adresse vue plus haut doit être adaptée pour tenir compte des tiles. Pour cela, il faut remplacer la taille d'un texel par la taille d'une tile, et que la largeur de la texture soit exprimée en nombre de tiles. De plus, on doit adapter les coordonnées des texels pour donner des coordonnées de tile. Généralement, les tiles sont des carrés de N pixels de côté, ce qui fait qu'on peut regrouper les lignes et les colonnes par paquets de N. Il suffit donc de diviser Y et X pour obtenir les coordonnées de la tile, de même que la larguer. La formule pour calculer la position de la énième tile est alors la suivante :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + \text{Taille mémoire d'une tile} \times \left( {\text{Width} \over N} \times {Y \over N} + {X \over N} \right)</math>
On peut réécrire le tout comme suit :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + K \times \left( {Y \over N} + X \right)</math>, avec K une constante connue à la compilation des shaders.
Vu que les tiles sont carrées avec une largeur qui est une puissance de deux, la multiplication par la taille d'une tile en mémoire se simplifie : on passe d'une multiplication entière à des décalages de bits. Même chose pour le calcul de l'adresse de la tile à partir des coordonnées x,y : ils impliquent des divisions par une puissance de deux, qui deviennent de simples décalages.
La position d'un pixel dans une tile dépend du format de la texture, mais peut se calculer avec quelques calculs arithmétiques simples. Dans les cas les plus simples, les pixels sont mémorisés ligne par ligne, ou colonne par colonne. Mais ce n'est pas systématiquement le cas. Toujours est-il que les calculs pour déterminer l'adresse sont simples, et ne demandent que quelques additions ou multiplications. Mais avec les formats de texture utilisés actuellement, les tiles sont chargées en entier dans le cache de texture, sans compter que diverses techniques de compression viennent mettre le bazar, comme on le verra dans la suite de cours.
Un avantage de l'organisation en tiles est qu'elle se marie bien avec le parcours des textures. On peut parcourir une texture dans tous les sens, horizontal, vertical, ou diagonal, on sait que les prochains pixels ont de fortes chances d'être dans la même tile. Si on rentre dans une tile par la gauche en haut, on a encore quelques pixels à parcourir dans la tile, par exemple. De même, le filtrage de textures est facilité. On verra dans ce qui va suivre que le filtrage de texture a besoin de lire des blocs de 4 texels, des carrés de 2 pixels de côté. Avec l'organisation en tile, on est certain que les 4 texels seront dans la même tile, sauf s'ils ont le malheur d'être tout au bord d'une tile. Ce dernier cas est assez rare, et il l'est d'autant plus que les tiles sont grandes. Enfin, un dernier avantage est que les tiles sont généralement assez petites pour tenir tout entier dans une ligne de cache. Le cache de texture est donc utilisé à merveille, ce qui rend les accès aux textures plus rapides.
====Les textures basées sur des ''z-order curves''====
Les formats de textures théoriquement optimaux utilisent une '''''Z-order curve''''', illustrée ci-dessous. L'idée est de découper la texture en quatre rectangles identiques, et de stocker ceux-ci les uns à la suite des autres. L'intérieur de ces rectangles est lui aussi découpé en quatre rectangles, et ainsi de suite. Au final, l'ordre des pixels en mémoire est celui illustré ci-dessous.
[[File:Z-CURVE.svg|centre|vignette|upright=2|Construction d'une ''Z-order curve''.]]
Les texels sont stockés les uns à la suite des autres dans la mémoire, en suivant l'ordre donnée par la ''Z-order curve''. Le calcul d'adresse calcule la position du texel en mémoire, par rapport au début de la texture, et ajoute l'adresse du début de la texture. Mais tout le défi est de calculer la position d'un texel en mémoire, à partir des coordonnées x,y. Le calcul peut sembler très compliqué, mais il n'en est rien. Le calcul demande juste de regarder les bits des deux coordonnées et de les combiner d'une manière particulièrement simple. Il suffit de placer le bit de poids fort de la coordonnée x, suivi de celui de la coordonnée y, et de faire ainsi de suite en passant aux bits suivants.
[[File:Zcurve45bits.png|centre|vignette|upright=1.5|Calcul de la position d'un élément dans une ''Z-order curve'' à partir des coordonnées x et y.]]
L'avantage d'une telle organisation est que la textures est découpées en ''tiles'' rectangulaires d'une certaine taille, elles-mêmes découpées en ''tiles'' plus petites, etc. Et il se trouve que cette organisation est parfaite pour le cache de texture. L'idéal pour le cache de texture est de charger une ''tile'' complète dans le cache de textures. Quand on accède à un texel, on s'assure que la ''tile'' complète soit chargée. Mais cela demande de connaitre à l'avance la taille d'une ''tile''. Les formats de texture fournissent généralement une ''tile'' carré de 4 pixels de côté, mais cela donnerait un cache trop petit pour être vraiment utile. Avec cette méthode, on s'assure qu'il y ait une ''tile'' avec la taille optimale. Les ''tiles'' étant découpées en ''tiles'' plus petites, elles-mêmes découpées, et ainsi de suite, on s'assure que la texture est découpées en ''tiles'' de taille variées. Il y aura au moins une ''tile'' qui rentrera tout pile dans le cache.
===Les techniques de rendu à textures multiples===
Nous venons de voir comment une texture est plaquée sur un objet 3D, ou une surface comme un sol. Pour résumer, le calcul de l'adresse d'un texel prend la position du texel par rapport au début de la texture, et ajoute l'adresse du début de la texture. L'adresse mémoire de la texture est connue au moment où le pilote de la carte graphique place la texture dans la mémoire vidéo, et cette information est transmise au matériel par l'intermédiaire du processeur de commande, puis passée aux processeurs de shaders et à l'unité de texture. Le tout est couplé à d'autres informations, la plus importante étant la ''taille de la texture en octets'', pour éviter de déborder lors des accès à la texture.
Néanmoins, il s'agit là du cas le plus simple. Certaines techniques de rendu demandent de choisir la texture à plaquer parmi un ensemble de plusieurs textures. Les techniques en question sont assez variées et n'ont pas grand chose en commun. Les plus connues sont le ''mip-mapping'', le ''cube-mapping'' et les textures virtuelles. Le ''mip-mapping'' sert à filtrer les textures, chose qu'on expliquera plus tard, le ''cube-mapping'' sert à simuler des réflexions sur un objet en plaquant une texture de l'environnement dessus, les textures virtuelles sont une optimisation pour les textures des terrains de grande taille. Mais malgré leurs différences, elles demandent de choisir quelle texture plaquer entre plusieurs textures de base. En clair, l'adresse de base de la texture varie selon la situation. Voyons-les dans le détail.
==L'implémentation matérielle du placage de textures==
Pour résumer, la lecture d'un texel demande d'effectuer plusieurs étapes. Dans le cas le plus simple, sans ''mip-mapping'' ou ''cubemapping'', on doit effectuer les étapes suivantes :
* Il faut d'abord normaliser les coordonnées de texture pour qu'elles tombent dans l'intervalle [0,1] en fonction du mode d'adressage désiré.
* Ensuite, les coordonnées u,v doivent être converties en coordonnées entières, ce qui demande une multiplication flottante.
* Enfin, l'adresse finale est calculée à partir des coordonnées entières et en ajoutant l'adresse de base de la texture (et éventuellement avec d'autres calculs arithmétiques suivant le format de la texture).
Tout cela pourrait être fait par le pixel shaders, mais cela implique beaucoup de calculs répétitifs et d'opérations arithmétiques assez lourdes, avec des multiplications flottantes, des additions et des multiplications entières, etc. Faire faire tous ces calculs par les shaders serait couteux en performance, sans compter que les shaders deviendraient plus gros et que cela aurait des conséquences sur le cache d'instruction. De plus, certaines de ces étapes peuvent se faire en parallèle, comme les deux premières, ce qui colle mal avec l'aspect sériel des shaders.
Aussi, les processeurs de shaders incorporent une unité de calcul d'adresse spéciale pour faire ces calculs directement en matériel. L'unité de calcul en question est dans l'unité de texture. Cette dernière contient donc au minimum deux circuits : un circuit de calcul d'adresse, et un circuit d'accès à la mémoire. Toute la difficulté tient dans le calcul d'adresse, plus que dans le circuit de lecture. Le calcul d'adresse est conceptuellement réalisé en deux étapes. La première étape qui transforme les coordonnées u,v en coordonnées x,y qui donne le numéro de la ligne et de la colonne du texel dans la texture. La seconde étape prend ces deux coordonnées x,y, l'adresse de la texture, et détermine l'adresse de la tile à lire.
[[File:Unité de texture simple.png|centre|vignette|upright=2|Unité de texture simple]]
===La gestion des accès mémoire===
Enfin, l'unité de texture doit tenir compte du fait que la mémoire vidéo met du temps à lire une texture. En théorie, l'unité de texture ne devrait pas accepter de nouvelle demande de lecture tant que celle en cours n'est pas terminée. Mais faire ainsi demanderait de bloquer tout le pipeline, de l'''input assembler'' au unités de''shaders'', ce qui est tout sauf pratique et nuirait grandement aux performances.
Une solution alternative consiste à mettre en attente les demandes de lectures de texture pendant que la mémoire est occupée. La manière la plus simple d'implémenter des accès mémoire multiples est de les mettre en attente dans une petite mémoire FIFO. Cela implique que les accès mémoire s’exécutent dans l'ordre demandé par le ''shader'' et/ou l'unité de rastérisation, il n'y a pas de réorganisation des accès mémoire ou d’exécution dans le désordre des accès mémoire.
[[File:Texture prefetching.png|centre|vignette|upright=1.5|Accès mémoire simultanés.]]
Évidemment, quand la mémoire FIFO est pleine, le pipeline est alors totalement bloqué. Le rasteriser est prévenu que l'unité de texture ne peut pas accepter de nouvelle lecture de texture. En pratique, la FIFO est généralement d'une taille respectable et permet de mettre en attente beaucoup de demandes de lecture de texture. Il faut de plus noter qu'il y a une FIFO par processeur de ''shader'' sur les cartes graphiques modernes. Quand elle est pleine, le processeur cesse d'exécuter de nouveaux accès mémoire, mais peut continuer à exécuter des ''shaders'' dans les autres unités de calcul, pas besoin de bloquer complétement le pipeline.
===L'intégration du cache de textures===
Il faut noter que les unités de texture incorporent aussi un cache de texture, voire plusieurs. L'intégration des caches de texture avec la mémoire FIFO précédente est quelque peu compliqué, car il faut garantir que les lectures de texture se fassent dans le bon ordre. On ne peut pas exécuter une lecture dans le cache alors que des lectures précédentes sont en attente de lecture en mémoire vidéo. Et cela pose un gros problème : une lecture dans le cache de texture prend quelques dizaines de cycles d'horloge, alors qu'une lecture en mémoire vidéo en prend facilement 400 à 800 cycles, parfois plus. Et cela fait que l'ordre des accès mémoire peut s'inverser.
Prenons par exemple un accès au cache précédé et suivi par deux accès en mémoire vidéo. Le premier démarre au cycle 1, et se termine au cycle numéro 400. L'accès au cache commence au cycle 2 et se termine 20 cycles après, au cycle numéro 22. En clair, la lecture dans le cache s'est terminée avant l'accès mémoire qui le précède. Les textures ne sont donc plus lues dans l'ordre. Et il faut trouver une solution pour éviter cela.
La solution est de retarder les lectures dans le cache tant que tous les accès précédents ne sont pas terminés. Mais pour retarder les lectures en question, il faut d'abord savoir si la lecture atterrit dans le cache ou non, ce qui demande d'accéder au cache. On fait face à un dilemme : on veut retarder les accès au cache, mais les différencier des lectures déclenchant des accès mémoire demande d'accéder au cache en premier lieu. La solution est décrite dans l'article "Prefetching in a Texture Cache Architecture" par Igehy et ses collègues. Elle se base sur deux idées combinées ensemble.
La première idée est de séparer l'accès au cache en deux : une étape qui vérifie si les texels à lire sont dans le cache, et une étape qui accède aux données dans le cache lui-même. Un cache de texture est donc composé de deux circuits principaux. Le premier vérifie la présence des texels dans le cache. Il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Ensuite, en plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
La seconde idée est de retarder l'accès au cache entre les deux phases. La première étape d'un accès mémoire vérifie si la donnée est dans le cache ou non. Puis, on retarde la lecture des données, pour attendre que toutes les lectures précédentes soient terminées. Et enfin, troisième étape : la lecture des texels dans la mémoire cache proprement dite. Les accès mémoire passant par la mémoire vidéo se font de la même manière, à une différence près : la lecture dans le cache est remplacée par la lecture en mémoire vidéo. Tout démarre avec une demande à l'unité de tags, qui vérifie si le texel est dans le cache ou non. Puis on retarde l'accès tant que la mémoire vidéo est occupée, puis on effectue la lecture en mémoire vidéo.
Si ce n'est pas le cas, l'accès mémoire est envoyé à la mémoire vidéo comme précédemment, à savoir qu'il est mis en attente dans une mémoire FIFO, puis envoyé à la mémoire vidéo dès que celle-ci est libre. Mais en sortie de la mémoire, la donnée lue est envoyée dans le cache de texture, par dans l'unité de filtrage. Pour savoir où placer la donnée lue, l'unité de tag a réservé une ligne de cache précise, une adresse bien précise. L'adresse en question est disponible en lisant une autre mémoire FIFO, qui a mis en attente l'adresse en question, en attendant que l'accès mémoire se termine. La donnée est alors écrite dans le cache, puis lue par l'unité de filtrage de textures.
Pour une lecture dans le cache, le déroulement est similaire, mais sans le passage par la mémoire. La lecture fait une demande à l'unité de tag, et celle-ci répond que la donnée est bien dans le cache. Elle place alors l'adresse à lire dans la file d'attente. Une fois que les accès mémoire précédents sont terminés, l'adresse sort de la file d'attente et est envoyée à la mémoire de données. La lecture s'effectue, les texels sont envoyés à l'unité de filtrage de textures. La seule différence avec un ''phased cache'' normal est l'insertion de l'adresse à lire dans une FIFO qui vise à mettre en attente
[[File:Unité de texture avec un cache de texture.png|centre|vignette|upright=2.0|Unité de texture avec un cache de texture]]
Pour résumer, l'implémentation précédente garantit une exécution des lectures dans leur ordre d'arrivée. Et pour cela, elle retarde les lectures dans le cache tant que les lectures en mémoire précédentes ne sont pas terminées. L'accès au cache est plus rapide que l'accès en mémoire vidéo, mais le retard ajouté pour garantir l'ordre des lectures fait que le temps d'accès est très long.
==Le mip-mapping==
Le '''mip-mapping''' a pour but de légèrement améliorer les graphismes des objets lointains, tout en rendant les calculs de texture plus rapides. Formellement, le ''mip-mapping'' est une technique de filtrage de texture, mais nous l'abordons maintenant car elle est surtout liée au calcul d'adresse. Les unités de texture ont des circuits de filtrage de texture séparés des circuits de ''mip-mapping'' et de calcul d'adresse, d'où le fait que nous en parlons séparément.
Le problème résolu par le ''mip-mapping'' est le rendu des textures lointaines. Si une texture est plaquée sur un objet lointain, une bonne partie des détails est invisible pour l'utilisateur. Un pixel de l'écran est associé à plusieurs texels. Idéalement, la carte graphiques devrait lire tous ces texels et en faire une sorte de moyenne pondérée, pour calculer la couleur finale du pixel. Mais dans les faits, ce serait très gourmand et compliqué à implémenter en hardware. Une solution serait de ne garder que quelque texels, mais cela a tendance à créer des artefacts visuels (les textures affichées ont tendance à pixeliser). Le ''mip-mapping'' permet de réduire ces deux problèmes en même temps en précalculant cette moyenne pondérée pour des distances prédéfinies.
L'idée est d'utiliser plusieurs exemplaires d'une même texture à des résolutions différentes, chaque exemplaire étant adapté à une certaine distance. Par exemple, une texture sera stocké avec un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre de 128 * 128 et ainsi de suite jusqu’à un dernier exemplaire de 32 * 32 pixel. Chaque exemplaire correspond à un '''niveau de détail''', aussi appelé ''Level Of Detail'' (abrévié en LOD). La résolution utilisée diminue d'autant plus que l'objet est situé loin de la caméra. Les objets proches seront rendus avec la texture 512*512, ceux plus lointains seront rendus avec la texture de résolution 256*256, les textures 128*128 seront utilisées encore plus loin, et ainsi de suite jusqu'aux objets les plus lointains qui sont rendus avec la texture la plus petite de 32*32.
[[File:MipMap Example STS101.jpg|centre|vignette|upright=2|Exemples de mip-maps.]]
Le ''mip-mapping'' améliore grandement la qualité d'image. L'image d'exemple ci-dessous le montre assez bien.
[[File:Mipmapping example.png|centre|vignette|upright=2|Exemple de mipmapping.]]
Pour faciliter les calculs d'adresse, les LOD d'une même texture sont stockées les uns après les autres en mémoire (dans un tableau, comme diraient les programmeurs). Ainsi, pas besoin de se souvenir de la position en mémoire de chaque LOD : l'adresse de la texture de base, et quelques astuces arithmétiques suffisent. Prenons le cas où la texture de base a une taille L. le premier exemplaire est à l'adresse 0, le second niveau de détail est à l'adresse L, le troisième à l'adresse L + L/4, le suivant à l'adresse L + L/4 + L/16, et ainsi de suite. Le calcul d'adresse demande juste connaître le niveau de détails souhaité et l'adresse de base de la texture. Le niveau de détail voulu est calculé par les pixel shaders, en fonction de la coordonnée de profondeur du pixel à traiter.
Évidemment, cette technique consomme de la mémoire vidéo, vu que chaque texture est dupliquée en plusieurs exemplaires, en plusieurs LOD. Dans le détail, la technique du mip-mapping prend au maximum 33% de mémoire en plus (sans compression). Cela vient du fait qu'en prenant une texture dexu fois plus petite, elle prend 4 fois moins de mémoire : 2 fois moins de pixels en largeur, et 2 fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des LODs, texture de base comprise, prendra X + (X/4) + (X/16) + (X/256) + … Un petit calcul de limite donne 4/3 * X, soit 33% de plus.
===L'implémentation du mip-mapping dans l'unité de texture===
Le ''mip-mapping'' est lui aussi pris en charge par l'unité de calcul d'adresse, car cette technique change l'adresse de base de la texture. La gestion du ''mip-mapping'' est cependant assez complexe. Il est possible de laisser le pixel shader calculer quel niveau de détail utiliser, en fonction de la coordonnée de profondeur z du pixel à afficher. La carte graphique détermine alors automatiquement quelle texture lire, quel niveau de détail, automatiquement. Elle détermine aussi la bonne résolution pour la texture, qui est égal à la résolution de la texture de base, divisée par le niveau de détail. Pour résumer, le niveau de détail est envoyé aux unités de texture, qui s'occupent de calculer l'adresse de base et la résolution adéquates. Quelques calculs arithmétiques simples, donc, qui s'implémentent facilement avec quelques circuits.
Mais une autre méthode laisse la carte graphique déterminer le niveau de détail par elle-même. Dans ce cas, cela demande, outre les deux coordonnées de texture, de calculer la dérivée de ces deux coordonnées dans le sens horizontal et vertical, ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Les quatre dérivées sont les suivantes :
: <math>\frac{du}{dx}</math>, <math>\frac{dv}{dx}</math>, <math>\frac{du}{dy}</math>, <math>\frac{dv}{dy}</math>
Un bon moyen pour obtenir les dérivées demande de regrouper les pixels par groupes de 4 et de faire la différence entre leurs coordonnées de texture respectives. On peut calculer les deux dérivées horizontales en comparant les deux pixels sur la même ligne, et les deux dérivées verticales en comparant les deux pixels sur la même colonne. Mais cela demande de rastériser les pixels par groupes de 4, par ''quads''. Et c'est ce qui est fait sur les cartes graphiques actuelles, qui rastérisent des groupes de 4 pixels à la fois.
[[File:Texture sampler unit with mipmapping.png|centre|vignette|upright=2.0|Unité de texture avec mipmapping.]]
Malheureusement, le calcul exact utilisé pour le choix de la mip-map dépend du GPU considéré et peu de chose est connu quant à ces algorithmes. Il est possible d'inférer le comportement à partir d'observations, mais guère plus. Pour ceux qui veulent en savoir plus, je conseille la lecture de cet article de blog :
* [https://pema.dev/2025/05/09/mipmaps-too-much-detail/ Mipmap selection in too much detail]
==Le cube-mapping==
[[File:Cube mapped reflection example 2.JPG|vignette|Exemple de reflets environnementaux.]]
L''''environnement-mapping''' est une technique de calcul de divers effets graphiques liés à l'environnement, notamment des réflexions. L'idée est de plaquer une texture pré-calculée pour simuler l'effet de l'environnement sur une surface ou un objet 3D. Il en existe plusieurs versions différentes, mais la seule utilisée de nos jours est le ''cube-mapping'', où la texture de l'environnement est plaquée sur un cube, d'où son nom. Le cube en question est utilisé différemment suivant ce que l'on cherche à faire avec le ''cube-mapping''. Les deux utilisations principales sont le rendu du ciel et des décors, et les réflexions sur la surface des objets. Dans les deux cas, l'idée est de précalculer ce que l'on voit du point de vue de la caméra. On place la caméra dans la scène 3D, on place un cube centré sur la caméra, le cube est texturé avec ce que l'on voit de l'environnement depuis la caméra/l'objet de son point de vue.
[[File:Panorama cube map.png|centre|vignette|upright=2|L'illustration montre en premier lieu une ''cubemap'' avec les six faces mises en évidence, puis quel environnement 3D elle permet de simuler, le troisième illustration montrant comment la ''cubemap'' est utilisée pour simuler l'environnement.]]
Le rendu du ciel et des décors lointains dans les jeux vidéo se base sur des '''''skybox''''', à savoir un cube centré sur la caméra, sur lequel on ajoute des textures de ciel ou de décors lointains. Le cube est recouvert par une texture, qui correspond à ce que l'on voit quand on dirige le regard de la caméra vers cette face. Contrairement à ce qu'on pourrait croire, la skybox n'est pas les limites de la scène 3D, les limites du niveau d'un jeu vidéo ou quoique ce soit d'autre de lié à la physique de la scène 3D. La skybox est centrée sur la caméra, elle suit la caméra dans son mouvement. Centrer la skybox sur la caméra permet de simuler des décors très lointains, suffisamment lointain pour qu'on n'ait pas l'illusion de s'en rapprocher en se déplaçant dans la map. De plus, cela évite d'avoir à faire trop de calculs à chaque fois que l'on bouge la caméra. La texture plaquée sur le cube est une texture unique, elle-même découpée en six sous-textures, une par face du cube.
[[File:Skybox example.png|centre|vignette|upright=2|Exemple de Skybox.]]
[[File:Cube mapped reflection example.jpg|vignette|Réflexions calculées par une ''cubemap''.]]
Le ''cube-mapping'' est aussi utilisé pour des reflets. L'idée est de simuler les reflets en plaquant une texture pré-calculée sur l'objet réflecteur. La texture pré-calculée est un dessin de l'environnement qui se reflète sur l'objet, un dessin du reflet à afficher. En la plaquant la texture sur l'objet, on simule ainsi des reflets de l'environnement, mais on ne peut pas calculer d'autres reflets comme les reflets objets mobiles comme les personnages. Et il se trouve que la texture pré-calculée est une ''cubemap''. Pour les environnements ouverts, c'est la ''skybox'' qui est utilisée, ce qui permet de simuler les reflets dans les flaques d'eau ou dans des lacs/océans/autres. Pour les environnements intérieurs, c'est une cubemap spécifique qui utilisée. Par exemple, pour l'intérieur d'une maison, on a une ''cubemap'' par pièce de la maison. Les reflets se calculent en précisant quelle ''cubemap'' appliquer sur l'objet en fonction de la direction du regard.
[[File:Cube map level.png|centre|vignette|Cube map de l'intérieur d'une pièce d'un niveau de jeux vidéo.]]
Toujours est-il que les textures utilisées pour le ''cubemapping'', appelées des ''cubemaps'', sont en réalité la concaténation de six textures différentes. En mémoire vidéo, la ''cubemap'' est stockée comme six textures les unes à la suite des autres. Lors du rendu, on doit préciser quelle face du cube utiliser, ce qui fait 6 possibilités. On a le même problème qu'avec les niveaux de détail, sauf que ce sont les faces d'une ''cubemap'' qui remplacent les textures de niveaux de détails. L'accès en mémoire doit donc préciser quelle portion de la ''cubemap'' il faut accéder. Et l'accès mémoire se complexifie donc. Surtout que l'accès en question varie beaucoup suivant l'API graphique utilisée, et donc suivant la carte graphique.
Le support des ''cubemaps'' dépend de l'API 3D, et surtout de si elle date ou si elle est récente :
* Les API 3D très anciennes ne gérent pas nativement les ''cubemaps'', qui doivent être émulées en logiciel en utilisant six textures différentes. Le pixel shader décide donc quelle ''cubemap'' utiliser, avec quelques calculs sur la direction du regard.
* Les API 3D récentes gèrent nativement les ''cubemaps''. Pour les versions les plus vielles de ces API, les six faces sont numérotées et l'accès à une ''cubemap'' précise quel face utiliser en donnant son numéro. La carte graphique choisit alors automatiquement la bonne texture. Mais cela demande de laisser le calcul de la bonne face au pixel shader.
* Dans les API 3D modenres, les ''cubemap'' sont gérées comme des textures en trois dimensions, adressées avec trois coordonnées u,v,w. La carte graphique utilise ces trois coordonnées de manière à en déduire quelle est la face pertinente, mais aussi les coordonnées u,v dans la texture de la face.
==Le filtrage de textures==
Plaquer des textures sans autre forme de procès ne suffit pas à garantir des graphismes d'une qualité époustouflante. La raison est que les sommets et les texels ne tombent pas tout pile sur un pixel de l'écran : le sommet associé au texel peut être un petit peu trop en haut, ou trop à gauche, etc. Une explication plus concrète fait intervenir les coordonnées de texture. Souvenez-vous que lorsque l'on traduit une coordonnée de texture u,v en coordonnées x,y, on obtient un résultat qui ne tombe pas forcément juste. Souvent, le résultat a une partie fractionnaire. Si celle-ci est non-nulle, cela signifie que le texel/sommet n'est pas situé exactement sur le pixel voulu et que celui-ci est situé à une certaine distance. Concrètement, le pixel tombe entre quatre texels, comme indiqué ci-dessous.
[[File:Filtrage texture.png|centre|vignette|upright=2.0|Position du pixel par rapport aux texels.]]
Pour résoudre ce problème, on doit utiliser différentes techniques d'interpolation, aussi appelées techniques de '''filtrage de texture''', qui visent à calculer la couleur du pixel final en fonction des texels qui l'entourent. Il existe de nombreux types de filtrage de textures, qu'il s'agisse du filtrage linéaire, bilinéaire, trilinéaire, anisotropique et bien d'autres.
Tous ont besoin d'avoir certaines informations qui sont généralement fournies par les circuits de calcul d'adresse. La première est clairement la partie fractionnaire des coordonnées x,y. La seconde est la dérivée de ces deux coordonnées dans le sens horizontal et vertical., ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Toujours est-il que le filtrage de texture est une opération assez lourde, qui demande beaucoup de calculs arithmétiques. On pourrait en théorie le faire dans les pixels shaders, mais le cout en performance serait absolument insoutenable. Aussi, les cartes graphiques intègrent toutes un circuit dédié au filtrage de texture, le ''texture sampler''. Même les plus anciennes cartes graphiques incorporent une unité de filtrage de texture, ce qui nous montre à quel point cette opération est importante.
[[File:Texture unit.png|centre|vignette|upright=2.0|Unité de texture.]]
On peut configurer la carte graphique de manière à ce qu'elle fasse soit du filtrage bilinéaire, soit du filtrage trilinéaire, on peut configurer le niveau de filtrage anisotropique, etc. Cela peut se faire dans les options de la carte graphique, mais cela peut aussi être géré par l'application. La majorité des jeux vidéos permettent de régler cela dans les options. Ces réglages ne concernent pas la texture elle-même, mais plutôt la manière dont l'unité de texture doit fonctionner. Ces réglages sur l''''état de l'unité de texture''' sont mémorisés quelque part, soit dans l'unité de texture elle-même, soit fournies avec la ressource de texture elle-même, tout dépend de la carte graphique. Certaines cartes graphiques mémorisent ces réglages dans les unités de texture ou dans le processeur de commande, et tout changement demande alors de réinitialiser l'état des unités de texture, ce qui prend un peu de temps. D'autres placent ces réglages dans les ressources de texture elles-mêmes, ce qui rend les modifications de configuration plus rapides, mais demande plus de circuits. D'autres cartes graphiques mélangent les deux options, certains réglages étant globaux, d'autres transmis avec la texture. Bref, difficile de faire des généralités, tout dépend du matériel et le pilote de la carte graphique cache tout cela sous le tapis.
Maintenant que cela est dit, voyons quelles sont les différentes méthodes de filtrage de texture et comment la carte graphique fait pour les calculer.
===Le filtrage au plus proche===
La méthode de filtrage la plus simple consiste à colorier avec le texel le plus proche. Cela revient tout simplement à ne pas tenir compte de la partie fractionnaire des coordonnées x,y, ce qui est très simple à implémenter en matériel. C'est ce que l'on appelle le '''filtrage au plus proche''', aussi appelé ''nearest filtering''.
Autant être franc, le résultat est assez pixelisé et peu agréable à l’œil. Par contre, le résultat est très rapide à calculer, vu qu'il ne demande aucun calcul à proprement parler. Elle ne fait pas appel à la parti fractionnaire des coordonnées entières de texture, ni aux dérivées de ces coordonnées. On peut combiner cette technique avec le mip-mapping, ce qui donne un résultat bien meilleur, bien que loin d'être satisfaisant. Au passage, toutes les techniques de filtrage de texture peuvent se combiner avec du mip-mapping, certaines ne pouvant pas faire sans.
[[File:Interpolation-nearest.svg|centre|vignette|Filtrage de texture au plus proche.]]
===Le filtrage linéaire===
Le filtrage le plus simple est le '''filtrage linéaire'''. Il effectue une interpolation linéaire entre deux mip-maps, deux niveaux de détails. Pour comprendre l'idée, nous allons prendre une situation très simple, avec une texture carrée de 512 texels de côté. Le mip-mapping crée plusieurs textures : une de 256 texels de côté, une de 128 texels, une de 64, etc. Maintenant, la texture est sur un objet à une certaine distance de l'écran, vu de face. Le résultat est qu'elle correspond à l'écran à un carré de 300 pixels de côté (pas d'erreur : pixels, pas texels). Dans ce cas, la texture se trouve entre deux mip-maps : celle de 512 pixels de côté, celle de 256. Laquelle choisir ? Le filtrage au plus proche prend la texture de 512 pixels de côté. Le filtrage linéaire lui, fait autrement.
Vu que la texture est entre deux mip-maps, l'idée est de prendre le texel au plus proche dans chaque texture et de faire une sorte de moyenne appelée l'interpolation linéaire. L'interpolation par du principe que la couleur varie entre les deux texels en suivant une fonction affine, illustrée ci-dessous. Ce ne serait évidemment pas le cas dans le monde réel, mais on supposer cela donne une bonne approximation de ce à quoi ressemblerait une texture à plus haute résolution. On peut alors calculer la couleur du pixel par une simple moyenne pondérée par la distance. Le résultat est que les transitions entre deux niveaux de détails sont plus lisses, moins abruptes.
[[File:Lin interp -é.png|centre|vignette|upright=2.0|Interpolation linéaire.]]
===Le filtrage bilinéaire===
Le filtrage bilinéaire effectue une sorte de moyenne pondérée des quatre texels les plus proches du pixel à afficher. Pour cela, rappelez-vous ce qui a été dit plus haut : les coordonnées x,y d'un pixel ont une partie entière et une partie fractionnaire. Le filtrage au plus proche élimine les parties fractionnaires, ce qui donne une coordonnée x,y. Avec le filtrage bilinéaire, on prend les texels de coordonnées (x,y) ; (x+1,y) ; (x,y+1) ; (x+1,y+1), le pixel étant entre ces 4 texels.
Mais le filtrage ne fait pas qu'une simple moyenne, il prend en compte les parties fractionnaires pour faire la moyenne. En effet, le pixel n'est pas au milieu du carré de texel, il est quelque part mais est souvent plus proche d'un texel que des autres. Et il faut donc pondérer la moyenne par les distances aux 4 texels. Pour cela, la moyenne est calculée à partir d'interpolations linéaires. Avec 4 pixels, nous allons devoir calculer la couleur de deux points intermédiaires. La couleur de ces deux points se calcule par interpolation linéaire, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.
[[File:Bilin3.png|centre|vignette|upright=2|Filtrage bilinéaire de texture.]]
Le circuit qui permet de faire l'interpolation bilinéaire est particulièrement simple. On trouve un circuit de chaque pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu, et un pour la transparence. Chacun de ces circuit est composé de sous-circuits chargés d'effectuer une interpolation linéaire, reliés comme suit.
[[File:Texture sampler unit.png|centre|vignette|Unité de filtrage bilinéaire.]]
Vous noterez que le filtrage bilinéaire accède à 4 pixels en même temps. Fort heureusement, les textures sont stockées de manière à ce qu'on puisse charger les 4 pixels en une fois, comme on l'a vu plus haut. Le filtrage bilinéaire a de fortes chances que les 4 pixels filtrés soient dans la même ''tile'', la seule exception étant quand ils sont tout juste sur le bord d'une ''tile''.
: La console de jeu Nintendo 64 n'utilise que trois pixels au lieu de quatre dans son interpolation bilinéaire, qui en devient une interpolation quasi-bilinéaire. La raison derrière ce choix est une question de performances, comme beaucoup de décisions de ce genre. Le résultat est un rendu imparfait de certaines textures.
===Le filtrage trilinéaire===
Avec le filtrage bilinéaire, des discontinuités apparaissent sur certaines surfaces. Par exemple, pensez à une texture de sol : elle est appliquée plusieurs fois sur toute la surface du sol. A une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512*512 à 256*256, ce qui est visible pour un joueur attentif. De telles transitions sont lissées grâce au filtrage linéaire, il n'y a plus qu'à le combiner avec le filtrage bilinéaire. Rien d’incompatible : le premier filtre l'intérieur d'une mip-map, le second combine deux mip-maps.
Le filtrage trilinéaire prend les deux mip-maps les plus proches, fait un filtrage bilinéaire avec chacune, puis fait une « une moyenne » pondérée entre les deux résultats. Le circuit de filtrage trilinéaire existe en plusieurs versions. La plus simple, illustrée ci-dessous, effectue deux filtrages bilinéaires en parallèle, dans deux circuits séparés, puis combine leurs résultats avec un circuit d'interpolation linéaire. Mais ce circuit nécessite de charger 8 texels simultanément. Qui plus est, ces 8 texels ne sont pas consécutifs en mémoire, car ils sont dans deux niveaux de détails/mip-maps différents.
[[File:Parallel trilinear filtering.png|centre|vignette|upright=2.0|Unité de filtrage trilinéaire parallèle.]]
Vu qu'on lit des texels dans deux mip-maps, les texels sont lus en deux fois : 4 texels provenant de la première mip-map, suivis par les 4 texels de l'autre mip-map. Les 4 premiers texels doivent donc être mis en attente dans des registres, en attendant que les 4 autres arrivent. Une amélioration du circuit précédent gère cela en ajoutant des registres. Il lit les 4 premiers texels, les filtre avec une interpolation bilinéaire, et mémorise le résultat dans un registre. Puis, il lit les 4 autres texels, les filtre, et met le résultat dans un second registre. A ce moment là, un circuit d'interpolation linéaire finit le travail. On économise donc un circuit d'interpolation bilinéaire, sans que les performances soient trop impactées.
[[File:Filtrage trilineaire.png|centre|vignette|upright=1.0|Unité de filtrage trilineaire série.]]
Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le temps où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. À chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients, et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.
===Le filtrage anisotrope===
D'autres artefacts peuvent survenir lors de l'application d'une texture, la perspective pouvant déformer les textures et entraîner l'apparition de flou. La raison à cela est que les techniques de filtrage de texture précédentes partent du principe que la texture est vue de face. Prenez une texture carrée, par exemple. Vue de face, elle ressemble à un carré sur l'écran. Mais tournez la caméra, de manière à voir la texture de biais, avec un angle, et vous verrez que la forme de la texture sur l'écran est un trapèze, pas un carré. Cette déformation liée à la perspective n'est pas prise en compte par les méthodes de filtrage de texture précédentes. Pour le dire autrement, les techniques de filtrage précédentes partent du principe que les 4 texels qui entourent un pixel forment un carré, ce qui est vrai si la texture est vue de face, sans angle, mais ne l'est pas si la texture n'est pas perpendiculaire à l'axe de la caméra. Du point de vue de la caméra, les 4 texels forment un trapèze d'autant moins proche d'un carré que l'angle est grand.
Pour corriger cela, les chercheurs ont inventé le '''filtrage anisotrope'''. En fait, je devrais plutôt dire : LES filtrages anisotropes. Il en existe un grand nombre, dont certains ne sont pas utilisés dans les cartes graphiques actuelles, soit car ils trop gourmand en accès mémoires et en calculs pour être efficaces, soit car ils ne sont pas pratiques à mettre en œuvre. Il est très difficile de savoir quelles sont les techniques de filtrage de texture utilisées par les cartes graphiques, qu'elles soient récentes ou anciennes. Beaucoup de ces technologies sont brevetées ou gardées secrètes, et il faudrait vraiment creuser les brevets déposés par les fabricants de GPU pour en savoir plus. Les algorithmes en question seraient de plus difficiles à comprendre, les méthodes mathématiques cachées derrière ces méthodes de filtrage n'étant pas des plus simple.
[[File:Anisotropic filtering en.png|centre|vignette|upright=2|Exemple de filtrage anisotrope.]]
==La compression de textures==
Les textures les plus grosses peuvent aller jusqu'au mébioctet, ce qui est beaucoup. Pour limiter la casse, les textures sont compressées. La '''compression de texture''' réduit la taille des textures, ce qui peut se faire avec ou sans perte de qualité. Elle entraîne souvent une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande. Mais il s'agit là d'une technique très simple, beaucoup plus simple que les techniques que nous allons voir dans cette section. Nous allons voir quelque algorithmes de compression de textures de complexité intermédiaire, mais n'allons pas voir l'état de l'art. Il existe des formats de texture plus récents que ceux qui nous allons aborder, comme l{{'}}''Ericsson Texture Compression'' ou l{{'}}''Adaptive Scalable Texture Compression'', plus complexes et plus efficaces.
Notons que les textures sont compressées dans les fichiers du jeu, mais aussi en mémoire vidéo. Les textures sont décompressées lors de la lecture. Pour cela, la carte graphique contient alors un circuit, capable de décompresser les textures lorsqu'on les lit en mémoire vidéo. Les cartes graphiques supportent un grand nombre de formats de textures, au niveau du circuit de décompression. Du fait que les textures sont décompressées à la volée, les techniques de compression utilisées sont assez particulières. La carte graphique ne peut pas décompresser une texture entière avant de pouvoir l'utiliser dans un ''pixel shader''. A la place, on doit pouvoir lire un morceau de texture, et le décompresser à la volée. On ne peut utiliser les méthodes de compression du JPEG, ou d'autres formats de compression d'image. Ces dernières ne permettent pas de décompresser une image morceau par morceau.
Pour permettre une décompression/compression à la volée, les textures sont des textures tilées, généralement découpées en tiles de 4 * 4 texels. Les ''tiles'' sont compressées indépendamment les unes des autres. Et surtout, avec ou sans compression, la position des tiles en mémoire ne change pas. On trouve toujours une tile tous les T octets, peu importe que la tile soit compressée ou non. Par contre, une tile compressée n'occupera pas T octets, mais moins, là où une tile compressée occupera la totalité des T octets. En clair, compresser une tile fait qu'il y a des vides entre deux tiles dans al mémoire vidéo, mais ne change rien à leur place en mémoire vidéo qui est prédéterminée, peu importe que la texture soit compressée ou non. L'intérêt de la compression de textures n'est pas de réduire la taille de la texture en mémoire vidéo, mais de réduire la quantité de données à lire/écrire en mémoire vidéo. Au lieu de lire T octets pour une tile non-compressée, on pourra en lire moins.
===La palette indicée et la technique de ''Vector quantization''===
La technique de compression des textures la plus simple est celle de la '''palette indicée''', que l'on a entraperçue dans le chapitre sur les cartes d'affichage. La technique de '''''vector quantization''''' peut être vue comme une amélioration de la palette, qui travaille non pas sur des texels, mais sur des ''tiles''. À l'intérieur de la carte graphique, on trouve une table qui stocke toutes les ''tiles'' possibles. Chaque ''tile'' se voit attribuer un numéro, et la texture sera composé d'une suite de ces numéros. Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l’embarqué utilisent ce genre de compression.
===Les algorithmes de ''Block Truncation coding''===
La première technique de compression élaborée est celle du '''''Block Truncation Coding''''', qui ne marche que pour les images en niveaux de gris. Le BTC ne mémorise que deux niveaux de gris par ''tile'', que nous appellerons couleur 1 et couleur 2, les deux niveaux de gris n'étant pas le même d'une ''tile'' à l'autre. Chaque pixel d'une ''tile'' est obligatoirement colorié avec un de ces niveaux de gris. Pour chaque pixel d'une ''tile'', on mémorise sa couleur avec un bit : 0 pour couleur 1, et 1 pour couleur 2. Chaque ''tile'' est donc codée par deux entiers, qui codent chacun un niveau de gris, et une suite de bits pour les pixels proprement dit. Le circuit de décompression est alors vraiment très simple, comme illustré ci-dessous.
[[File:Block Truncation coding.jpg|centre|vignette|upright=2.0|Block Truncation coding.]]
La technique du BTC peut être appliquée non pas du des niveaux de gris, mais pour chaque composante Rouge, Vert et Bleu. Dans ces conditions, chaque ''tile'' est séparée en trois sous-''tiles'' : un sous-bloc pour la composante verte, un autre pour le rouge, et un dernier pour le bleu. Cela prend donc trois fois plus de place en mémoire que le BTC pur, mais cela permet de gérer les images couleur.
===Le format de compression S3TC / DXTC===
L'algorithme de '''Color Cell Compression''', ou CCC, améliore le BTC pour qu'il gère des couleurs autre que des niveaux de gris. Ce CCC remplace les deux niveaux de gris par deux couleurs. Une ''tile'' est donc codée avec un entier 32 bits par couleur, et une suite de bits pour les pixels. Le circuit de décompression est identique à celui utilisé pour le BTC.
[[File:Color Cell Compression.jpg|centre|vignette|Color Cell Compression.]]
[[File:Dxt1-memory-layout.png|vignette|Dxt1 et ''color cell compression''.]]
Le format de compression de texture utilisé de base par Direct X, le DXTC, est une version amliorée de l'algorithme précédent. Il est décliné en plusieurs versions : DXTC1, DXTC2, etc. La première version du DXTC est une sorte d'amélioration du CCC : il ajoute une gestion minimale de transparence, et découpe la texture à compresser en ''tiles'' de 4 pixels de côté. La différence, c'est que la couleur finale d'un texel est un mélange des deux couleurs attribuée au bloc. Pour indiquer comment faire ce mélange, on trouve deux bits de contrôle par texel.
Si jamais la couleur 1 < couleur2, ces deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (2 * Couleur1 + Couleur2) / 3
* 11 = (Couleur1 + 2 * Couleur2) / 3
Sinon, les deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (Couleur1 + Couleur2) / 2
* 11 = Transparent
[[File:DXTC.jpg|centre|vignette|DXTC.]]
Le circuit de décompression du DXTC ressemble alors à ceci :
[[File:Circuit de décompression du DXTC.jpg|centre|vignette|upright=2.0|Circuit de décompression du DXTC.]]
===Les format DXTC 2, 3, 4 et 5 : l'ajout de la transparence===
Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3, lui-même replacé par le DXT4 et par le DXT5. Dans le DXT3, la transparence fait son apparition. Pour cela, on ajoute 64 bits par ''tile'' pour stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1.
[[File:Dxt23-memory-layout.png|centre|vignette|Dxt 2 et 3.]]
Dans le DXT4 et le DXT5, la méthode utilisée pour compresser les couleurs l'est aussi pour les valeurs de transparence. L'information de transparence est stockée par un en-tête contenant deux valeurs de transparence, le tout suivi d'une matrice qui attribue trois bits à chaque texel. En fonction de la valeur des trois bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1.
[[File:Dxt45-memory-layout.png|centre|vignette|Dxt 4 et 5.]]
===Le format de compression PVRTC===
Passons maintenant à un format de compression de texture un peu moins connu, mais pourtant omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle travaille surtout dans les cartes graphiques embarquées. Ses cartes se trouvent notamment dans l'ipad, l'iPhone, et bien d'autres smartphones actuels.
Avec le PVRTC, les textures sont encore une fois découpées en ''tiles'' de 4 texels par 4, mais la ressemblance avec le DXTC s’arrête là. Chacque ''tile'' est codée avec :
* une couleur codée sur 16 bits ;
* une couleur codée sur 15 bits ;
* 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
* et un bit de modulation, qui permet de configurer l’interprétation des bits de mélange.
Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4*4.
==Annexe : le ''normal-mapping'' hardware==
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, parlons un peu du ''normal mapping'' et de son implémentation dans les unités de texture. Pour rappel, 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. Et pour comprendre quel est le rapport avec les textures, nous allons devoir faire quelques rappels sur l'éclairage par pixel.
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
L'éclairage se fait en utilisant de nombreux calculs, qu'on a détaillé dans le chapitre sur les bases du rendu 3D. Ceux-ci utilisent la normale d'un sommet, à savoir un vecteur orienté à la verticale de la surface. En théorie, il y a une normale par sommet, ce qui fait que les calculs d'éclairage doivent se faire au niveau géométrique, avant la rastérisation. Mais pour obtenir un éclairage de meilleure qualité, il y a des techniques qui calcule l'éclairage d'une scène 3D pixel par pixel.
Une première technique d'éclairage par pixel interpole les normales lors de l'étape de rastérisation. On obtient alors un éclairage de Phong. Une autre solution est celle du ''normal mapping''. Le '''''normal mapping''''' précalcule les normales d'une surface dans une texture, appelée la ''normal map''. L'éclairage est alors réalisé par un pixel shader, qui lit les normales depuis cette texture et fait les calculs d'éclairage avec.
===L'usage de textures non-compressées pour les ''normal maps''===
La ''normal map'' est une texture, donc. Mais ce n'est pas une texture comme une autre. Elle mémorise un vecteur pour chaque texel, pas une couleur. Il est possible d'utiliser les formats de textures non-compressés, même si cela ne donne pas de bons résultats. Après tout, un vecteur est codé par trois coordonnées x,y,z, il est possible de coder chacune avec un octet et de packager cela dans une texture RGB classique. La coordonnée x va dans la composante Rouge, la coordonnée Y dans la composante Bleu, et la coordonnée z dans la composante z.
Une première optimisation est de ne pas mémoriser les trois coordonnées, mais seulement deux d'entre elles et de calculer la troisième. En effet, les normales sont des vecteurs ''normalisés'', c'est à dire que leur longueur vaut 1, par construction. Vu que la taille est connue à l'avance, on peut en déduire la coordonnée z à partir des coordonnées x et y, avec l'usage du théorème de Pythagore. Par définition, <math>z^2 = 1 - x^2 - y^2</math>. Le calcul peut être fait dans le ''pixel shader'' sans problème.
: Pour être précis, il faut que les normales soient définies d'une certaine manière pour que ça marche. Les normales ne sont pas définies dans le même système de coordonnées que le modèle 3D, mais dans un autre système appelé l'''espace tangent''.
Notons que l'accès à la ''normal map'' se fait comme pour n'importe quelle texture : les texels sont lus en mémoire vidéo, puis le filtrage de texture est appliqué, et enfin le tout est envoyé au ''pixel shader''. En théorie, le filtrage effectue une sorte d'interpolation des normales automatique, ce qui permet de trouver la normale à un pixel précis, même s'il n'est pas sur un texel.
Les solutions précédentes demandent d'utiliser des textures non-compressées, qui utilisent beaucoup de mémoire vidéo. Utiliser des textures compressées pourrait sembler résoudre le problème. Mais l'usage de textures compressées marche très mal pour les ''normal maps'', elles ne permettent pas d'obtenir une qualité ou une compression suffisante. Aussi, des formats de texture dédiés aux ''normal maps'' ont été inventés.
===Le format de texture 3Dc===
Le problème est que le rendu final n'est pas très beau. Et les opérations de filtrage ne donnent pas de très bons résultats. Pour remédier à ces problèmes, des formats de texture spécialisés pour les ''normal map'' ont été inventés. Le premier d'entre eux est le '''3Dc''', aussi appelé BC5 pour ''Block Compression 5''. Il utilise un octet par texel, au lieu de trois avec une texture RGB normale non-compressée. Et surtout : il est géré en matériel, directement dans l'unité de texture, qui peut décompresser les textures 3Dc et les filtrer !
Avec le 3Dc, seules coordonnées sont mémorisées, la troisième est calculée, comme dit plus haut. Pour le reste, la compression de la ''normal map'' ressemble un peu à celle des textures. La ''normal map'' est découpée en blocs de 4 par 4 texels de côté, chacune étant encodée en tenant compte de certaines redondances. Dans un bloc, les coordonnées x sont dans un certain intervalle, allant de <math>x_{min}</math> à <math>x_{max}</math>, idem pour la coordonnée y qui est dans l'intervalle <math>y_{min}</math>, <math>y_{max}</math>. L'intervalle <math>x_{min}</math> à <math>x_{max}</math> est coupé en 8 parts égales, ce qui fait 8 valeurs possibles dans cet intervalle, chacune correspondant à une valeur x possible pour une normale du bloc. La normale est donc encodée en précisant quelle valeur est la bonne, en utilisant un indice de 3 bits pour cela. La valeur exacte de la coordonnée x se calcule à partir de l'indice comme suit :
: <math>x = x_{min} + \left( \frac{x_{min} - x_{min}}{8} \right) \times \text{indice} </math>
Pour encoder un bloc de 4 par 4 normales, il faut donc :
* Un octet pour chaque coordonnée <math>x_{min}</math>, <math>x_{max}</math>, <math>y_{min}</math>, <math>y_{max}</math>.
* 6 bits par normale : 3 pour encoder le décalage de la coordonnée x, 3 pour la coordonnée y.
Le tout permet d'encoder un bloc de 16 normales en seulement 128 bits, soit un octet par normale. Le ''pixel shader'' se débrouille pour décompresser un bloc. Il effectue notamment le calcul du dessus, pour retrouver la coordonnée x. Les calculs étant particulièrement simples, le cout en performance est très faible. Et ce d'autant plus qu'ils demandent juste de faire des additions et des multiplications entières, que l'unité scalaire peut faire en parallèle d'autres opérations plus gourmandes. Et le cout en calculs est de toute façon compensé par l'économie de mémoire et de bande passante lié à la compression : diviser la taille des données par trois, ca a un sacré impact !
==Annexe : les ''shadowmap'' hardware==
Les anciens GPU, notamment la Geforce FX, avaient des fonctionnalités spécifiques pour le calcul des ombres. Dans la plupart des jeux vidéos de l'époque, et même de maintenant, les ombres sont calculées avec la technique des ''shadowmap''. L'idée est assez simple sur le principe : un pixel est dans l'ombre quand il est invisible depuis une source de lumière.
L'idée est que le rendu est réalisé en plusieurs passes, avec une passe par source de lumière et une passe finale pour calculer l'image finale. Nous allons expliquer la technique avec une seule source de lumière, et allons utiliser l'exemple de la scène ci-dessous.
[[File:7fin.png|centre|vignette|Scène 3D d'exemple.]]
===La technique du ''shadowmapping''===
[[File:2shadowmap.png|vignette|Résultat de la première passe : ''shadowmap''..]]
La première passe rend l'image depuis le point de vue de la source de lumière. Cette première passe ne rend pas les couleurs de la scène, elle ne s'intéresse qu'à la profondeur des pixels. Le résultat est que l'image ne rend que le tampon de profondeur. Celui-ci est ensuite réutilisé comme texture pour la passe suivante. La texture en question est appelée la '''''shadownmap'''''.
La perspective utilisée, ainsi que le ''view frustrum'', dépend de la source de lumière. Pour une source de lumière qui émet un cône de lumière, le ''view frustrum'' de l'image rendue doit contenir tout le cone de lumière, et doit coller le plus possible à celui-ci. Pour une source directionnelle, comme le soleil, une perspective orthographique est utilisée.
La seconde passe rend l'image du point de vue de la caméra, pour rendre l'image finale. Elle rend l'image finale, qui est composée de pixels, chacun ayant une position à l'écran x,y, et une profondeur z. Les coordonnées sont transformées pour obtenir la position de ce pixel depuis le point de vue de la caméra. Une simple multiplication de matrice suffit, rien de bien compliqué, un shader peut le faire.
[[File:5failed.png|vignette|Résultat du test des comparaisons.]]
Après cette étape, on a alors les coordonnées x,y,z de ce pixel du point de vue de la caméra, et la ''shadowmap''. Il est alors possible d'accéder à la ''shadowmap'' au même endroit, à la même place que le pixel testé, aux mêmes coordonnées x,y. Si la profondeur du pixel est supérieure à celle de la shadowmap au même endroit, alors le pixel est situé derrière la surface visible, donc est dans l'ombre. Sinon, il n'est pas dans l'ombre. Le même procédé est répété sur chaque pixel de l'écran.
===Les optimisations hardware du ''shadowmapping''===
La technique des ''shadowmap'' demande donc de calculer une texture ''shadowmap'', puis de lire celle-ci et de faire des comparaisons de profondeur. Les GPU comme la Geforce FX intégraient du matériel dans les unités de texture pour faciliter ce travail. Les unités de texture pouvaient lire les ''shadowmap'', et faire la comparaison de profondeur toutes seules, elles avaient des circuits pour. Il suffisait de leur fournir le pixel à tester, ses coordonnées x,y,z, et l'adresse de la ''shadowmap''. Les unités de texture renvoyaient alors un résultat valant 0 ou 1 : 1 si le pixel est dans l'ombre, 0 sinon.
Elles pouvaient même effectuer du filtrage de texture sur les ''shadowmap''. Mais le filtrage était différent de celui utilisé sur les autres textures : moyenner des valeurs de profondeur ne marche pas bien. Elles utilisaient des techniques de filtrage différentes : elles faisaient les tests de comparaison, puis faisaient la moyenne des résultats. Ainsi, pour du filtrage bilinéaire, elles lisaient 4 texels dans la ''shadowmap'', puis faisaient 4 tests de comparaison, et moyennaient les 4 résultats.
==Annexe : les textures virtuelles==
Les '''textures virtuelles''' sont une optimisation des textures normales, qui visent à accélérer le rendu de terrains de grande taille. Imaginez par exemple un monde assez ouvert, comme un environnement en forêt ou en montagne, avec une grande distance de visibilité. Avec de tels terrains, le "sol" est recouvert par une texture de sol unique qui recouvre tout le terrain. Elle ne se répète pas, est de très grande taille, et peut parfois recouvrir toute la map ! Mais il n'y a pas assez de mémoire vidéo pour mémoriser la texture toute entière. La seule solution est la suivante : une partie de la texture est placée en mémoire vidéo, le reste est soit placé en mémoire RAM ou sur le disque dur.
Pour cela, le moteur de jeu utilise une optimisation ingénieuse, basée sur une observation assez basique : une bonne partie de la texture est visible, mais le reste est caché par des arbres, des habitations ou d'autres obstacles. Une optimisation possible de ne garder en mémoire vidéo que les portions visibles de la texture, pas les portions cachées. Une autre optimisation mélange textures virtuelles et ''mip-mapping''. L'idée est que pour les portions lointaines d'une texture, la texture utilisée est une ''mip-map'' de basse résolution. L'idée est alors de ne charger que la ''mip-map'' adéquate, pas les autres niveaux de détail. En clair, la texture de base n'est pas chargée en mémoire vidéo, mais la ''mip-map'' basse résolution l'est.
===Une texture à deux niveaux===
L'implémentation des textures virtuelles découpe les méga-textures en ''tiles'', en morceaux rectangulaires de taille modeste. En clair, le terrain est découpé en morceau rectangulaires/carrés. Seules les tiles nécessaires sont chargées en mémoire vidéo, pas les autres. Par exemple, les ''tiles'' non-visibles ne sont pas placées en mémoire vidéo, seules les ''tiles'' visibles le sont. De même, il y a une ''tile'' par niveau de mip-map : seul la tile correspondant le niveau adéquat est en mémoire vidéo, les autres niveaux de détail ne sont pas chargés. On peut faire une analogie avec la mémoire virtuelle, où les données sont découpées en pages, qui sont chargées en mémoire RAM à la demande, suivant les besoins, les données pouvant être swappées sur le disque dur si elles sont peu utilisées. Sauf qu'ici, il s'agit de textures qui sont découpées en pages chargées à la demande en mémoire vidéo, depuis la RAM système.
Une texture virtuelle est en réalité un système à deux niveaux : une liste de ''tiles'' et les ''tiles'' elles-mêmes. La liste de ''tiles'' est appelée un '''atlas de texture''', c'est un peu l'équivalent de la ''tilemap'' pour le rendu 2D. Rendre une texture demande de calculer quelle ''tile'' contient le texel à afficher, consulter la ''tile'' en question, puis récupérer le texel adéquat dans cette ''tile''. La ''tile'' est donc une texture, mais la texture à charger est choisie parmi un ensemble, qui est ici l'atlas de texture.
===L'implémentation : logicielle versus matérielle===
Les textures virtuelles ont été utilisées pour la première fois par les jeux Rage 1 et 2 d'IdSoftware, et quelques jeux ultérieurs comme DOOM 2016. IdSoftware les appelait des '''''mega-textures'''''. L'optimisation permettait des gains en performance assez impressionnants. Le jeu Rage 1 utilisait une texture carrée unique de 128k pixels de côté pour rendre le terrain. En théorie, une telle texture devrait prendre 64 giga-octets, mais le jeu tournait correctement avec 512 méga-octets de RAM, poussivement avec seulement 256 méga-octets de RAM.
De nos jours, les textures virtuelles sont supportées par beaucoup de jeux vidéos, les moteurs les plus courants gèrent de telles textures de manière logicielles. Mais quelques GPU récents supportent les textures virtuelles. Sur les GPU récents, l'atlas de texture est géré nativement par le matériel. Le GPU choisit quelle ''tile'', quelle texture choisir pour rendre le texel adéquat. Pour cela, le GPU calcule quelle ''tile'' charger, consulte l'atlas de texture, et lit la texture de ''tile'' adéquate.
Mais l'implémentation sur les GPU récents a de nombreuses limitations. La limitation la plus importante est que la taille des textures virtuelles ne peut pas dépasser la taille d'une texture normale, soit 32768 pixels de côté pour une texture carrée environ sur les GPU de 2020. De plus, le chargement d'une ''tile'' est très lent. En clair, dès qu'on veut changer de niveau de mip-map pour une tile, ou dès qu'une tile devient visible, le chargement de la tile peut facilement prendre plusieurs centaines de millisecondes. Le filtrage de texture est très complexe avec des textures virtuelles, ce qui fait que le filtrage de texture virtuelle est souvent soumis à des limitations que les textures normales n'ont pas, notamment pour le filtrage anisotropique.
{{NavChapitre | book=Les cartes graphiques
| prev=Le rasterizeur
| prevText=Le rasterizeur
| next=Les Render Output Target
| nextText=Les Render Output Target
}}{{autocat}}
j1lq633mspw7ifva3nlsdg1xengbt79
763422
763421
2026-04-10T19:29:11Z
Mewtow
31375
763422
wikitext
text/x-wiki
[[File:Texture mapping.png|vignette|''Texture mapping'']]
Les '''textures''' sont des images que l'on va plaquer sur la surface d'un objet, du papier peint en quelque sorte. Les cartes graphiques supportent divers formats de textures, qui indiquent comment les pixels de l'image sont stockés en mémoire : RGB, RGBA, niveaux de gris, etc. Une texture est donc composée de "pixels", comme toute image numérique. Pour bien faire la différence entre les pixels d'une texture, et les pixels de l'écran, les pixels d'une texture sont couramment appelés des ''texels''.
==Le placage de textures inverse==
Pour rappel, plaquer une texture sur un objet consiste à attribuer un texel à chaque sommet, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture.
Dans les faits, on n'utilise pas de coordonnées entières de ce type. Les coordonnées de texture sont deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. Le nom donnée à cette technique de description des coordonnées de texture s'appelle l''''''UV Mapping'''''.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Les API 3D modernes gèrent des textures en trois dimensions, ce qui ajoute une troisième coordonnée de texture notée w. Dans ce qui va suivre, nous allons passer les textures en trois dimensions sous silence. Elles ne sont pas très utilisées, la quasi-totalité des jeux vidéo et applications 3D utilisant des textures en deux dimensions. Par contre, le matériel doit gérer les textures 3D, ce qui le rend plus complexe que prévu. Il faut ajouter quelques circuits pour, de quoi gérer la troisième coordonnée de texture, etc.
Lors de la rastérisation, chaque fragment se voit attribuer un sommet, et donc la coordonnée de texture qui va avec. Si un pixel est situé pile sur un sommet, la coordonnée de texture de ce sommet est attribuée au pixel. Si ce n'est pas le cas, la coordonnée de texture finale est interpolée à partir des coordonnées des trois sommets du triangle rastérisé. L'interpolation en question a lieu dans l'étape de rastérisation, comme nous l'avons vu dans le chapitre précédent. Le fait qu'il y ait une interpolation fait que les coordonnées du pixel gagent à être des nombres flottants. On pourrait faire une interpolation avec des coordonnées de texture entières, mais les arrondis et autres imprécisions de calcul donneraient un résultat graphiquement pas terrible, et empêcheraient d'utiliser les techniques de filtrage de texture que nous verrons dans ce chapitre.
À partir de ces coordonnées de texture, la carte graphique calcule l'adresse du texel qui correspond, et se charge de le lire. Et toute la magie a lieu dans ce calcul d'adresse, qui part de coordonnées de texture flottante, pour arriver à une adresse mémoire. Le calcul de l'adresse du texel se fait en plusieurs étapes, que nous allons voir ci-dessous. La première étape convertit les coordonnées flottantes en coordonnées entières, qui disent à quel ligne et colonne se trouve le texel voulu dans la texture. L'étape suivante transforme ces coordonnées x,y entières en adresse mémoire.
===La normalisation des coordonnées===
J'ai dit plus haut que les coordonnées de texture sont des coordonnées flottantes, comprises entre 0 et 1. Mais il faut savoir que les pixels shaders peuvent modifier celles-ci pour mettre en œuvre certains effets graphiques. Et le résultat peut alors se retrouver en-dehors de l'intervalle 0,1. C'est quelque chose de voulu et qui est traité par la carte graphique automatiquement, sans que ce soit une erreur. Au contraire, la manière dont la carte graphique traite cette situation permet d'implémenter des effets graphiques comme des textures en damier ou en miroir.
[[File:Clamp tile.jpg|vignette|Clamp tile]]
Il existe globalement trois méthodes très simples pour gérer cette situation, qui sont appelés des '''modes d'adressage de texture'''.
* La première méthode est de faire en sorte que le résultat sature. Si une coordonnée est inférieur à 0, alors on la remplace par un zéro. Si elle est supérieure à 1, on la ramène à 1. Avec cette méthode, tout se passe comme si les bords de la texture étaient étendus et remplissaient tout l'espace autour de la texture. Le tout est illustré ci-dessous. Ce mode d'accès aux textures est appelé le '''''clamp'''''.
* Une autre solution retire la partie entière de la coordonnée, elle coupe tout ce qui dépasse 1. Pour le dire autrement, elle calcule le résultat modulo 1 de la coordonnée. Le résultat est que tout se passe comme si la texture était répétée à l'infini et qu'elle pavait le plan.
* Une autre méthode remplit les coordonnées qui sortent de l’intervalle 0,1 avec une couleur préétablie, configurée par le programmeur.
===La conversion des coordonnées de textures flottantes en adresse mémoire===
Une fois la normalisation effectuée, les coordonnées de texture sont utilisées pour lire le texel voulu. Pour cela, les coordonnées de texte sont transformées en adresse mémoire, adresse qui pointe sur le texel ayant ces cordonnées. Pour cela, la première étape est de transformer les coordonnées flottantes u,v en coordonnées entières x,y qui pointent sur un texel. Pour cela, il suffit de multiplier les coordonnées flottantes u,v par la résolution de la texture accédée. Pour un écran de résolution <math>\text{height,width}</math>, le calcul est le suivant :
: <math>x = u \times \text{width}</math>
: <math>y = v \times \text{height}</math>
Le résultat est un nombre avec une partie entière et une partie fractionnaire. La partie entière des deux coordonnées donne la position x,y voulue, et la partie fractionnaire est conservée pour le filtrage de textures, mais passons cela sous silence pour le moment.
La seconde étape prend les coordonnées entières x,y et calcule l'adresse mémoire du texel. L'adresse dépend de la position de la texture en mémoire, précisément de son début, son premier texel, mais aussi de la position du texel par rapport au début de la texture. Et calculer cette position intra-texture dépend de la manière dont les texels sont stockés en mémoire.
====Les textures naïves====
Les programmeurs qui lisent ce cours s'attendent certainement à ce que la texture soit stockée en mémoire ligne par ligne, ou colonne par colonne. Cela veut dire que le premier pixel en partant d'en haut à gauche est stocké en premier, puis celui immédiatement à sa droite, puis celui encore à droite, et ainsi de suite. Une fois qu'on arrive à la fin d'une ligne, on passe à la ligne suivante, en-dessous. Cette organisation ligne par ligne s'appele l'organisation '''''row major order'''''. On peut faire pareil, mais colonne par colonne, ce qui donne le '''''column major order'''''.
[[File:Speicheranordnung Feld.svg|centre|vignette|upright=2|Row et column major order.]]
Maintenant, supposons que la texture commence à l'adresse <math>A_\text{texture}</math>, qui est l'adresse du premier texel. La texture a une résolution de <math>\text{width}</math> texels de large et <math>\text{height}</math> texels de haut. Par définition, les coordonnées X et Y des texels commencent à 0, ce qui fait que le pixel en haut à gauche a les coordonnées 0,0.
L'adresse du pixel se calcule comme suit :
: <math>A_\text{pixel} = A_\text{texture} + (\text{taille d'une ligne en octets} \times Y) + (\text{taille d'un texel en octets} \times X)</math>
La taille d'un pixel en mémoire est notée T. La taille d'une ligne en mémoire est de <math>width \times T</math>, par définition, vu qu'elle fait <math>width</math> texels. On a donc :
: <math>A_\text{pixel} = A_\text{texture} + (width \times T \times Y) + (T \times X)</math>
La formule se réécrit comme suit :
: <math>A_\text{pixel} = A_\text{texture} + T \times (width \times Y + X)</math>
Le calcul d'adresse est donc assez simple. Malheureusement, les textures ne sont pas stockées de cette manière en mémoire vidéo. En effet, elle se marie mal avec les opérations de filtrage de texture que nous allons voir dans ce qui suit. Le filtrage d'un texel dépend de ses voisins du dessus et du dessous. Le fait que la texture n'est pas forcément parcourue ligne par ligne fait que stocker une texture ligne par ligne n'est pas l'idéal.
De même, les textures sont déformées par la perspective. L'affichage de la texture ne se fait alors pas ligne par ligne, mais en parcourant la texture en diagonale, l'angle de la diagonale correspondant approximativement à l'angle que fait la verticale de la texture avec le regard. Vu qu'on ne connait pas à l'avance l'angle que fera la diagonale de parcours, on doit ruser.
====Les textures tilées====
Une première solution à ce problème est celle des '''textures tilées'''. Avec ces textures, l'image de la texture est découpée en ''tiles'', des rectangles ou en carrés de taille fixe, généralement des carrés de 4 pixels de côté. Les tiles ont une largeur et une longueur égales, afin de simplifier les calculs : on divise X et Y par le même nombre. De plus, leur largeur et leur longueur sont une puissance de deux, afin de simplifier les calculs d'adresse. Les ''tiles'' sont alors mémorisée les unes après les autres dans le fichier de la texture.
[[File:Texture tilée.png|centre|vignette|upright=2|Texture tilée]]
La formule de calcul d'adresse vue plus haut doit être adaptée pour tenir compte des tiles. Pour cela, il faut remplacer la taille d'un texel par la taille d'une tile, et que la largeur de la texture soit exprimée en nombre de tiles. De plus, on doit adapter les coordonnées des texels pour donner des coordonnées de tile. Généralement, les tiles sont des carrés de N pixels de côté, ce qui fait qu'on peut regrouper les lignes et les colonnes par paquets de N. Il suffit donc de diviser Y et X pour obtenir les coordonnées de la tile, de même que la larguer. La formule pour calculer la position de la énième tile est alors la suivante :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + \text{Taille mémoire d'une tile} \times \left( {\text{Width} \over N} \times {Y \over N} + {X \over N} \right)</math>
On peut réécrire le tout comme suit :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + K \times \left( {Y \over N} + X \right)</math>, avec K une constante connue à la compilation des shaders.
Vu que les tiles sont carrées avec une largeur qui est une puissance de deux, la multiplication par la taille d'une tile en mémoire se simplifie : on passe d'une multiplication entière à des décalages de bits. Même chose pour le calcul de l'adresse de la tile à partir des coordonnées x,y : ils impliquent des divisions par une puissance de deux, qui deviennent de simples décalages.
La position d'un pixel dans une tile dépend du format de la texture, mais peut se calculer avec quelques calculs arithmétiques simples. Dans les cas les plus simples, les pixels sont mémorisés ligne par ligne, ou colonne par colonne. Mais ce n'est pas systématiquement le cas. Toujours est-il que les calculs pour déterminer l'adresse sont simples, et ne demandent que quelques additions ou multiplications. Mais avec les formats de texture utilisés actuellement, les tiles sont chargées en entier dans le cache de texture, sans compter que diverses techniques de compression viennent mettre le bazar, comme on le verra dans la suite de cours.
Un avantage de l'organisation en tiles est qu'elle se marie bien avec le parcours des textures. On peut parcourir une texture dans tous les sens, horizontal, vertical, ou diagonal, on sait que les prochains pixels ont de fortes chances d'être dans la même tile. Si on rentre dans une tile par la gauche en haut, on a encore quelques pixels à parcourir dans la tile, par exemple. De même, le filtrage de textures est facilité. On verra dans ce qui va suivre que le filtrage de texture a besoin de lire des blocs de 4 texels, des carrés de 2 pixels de côté. Avec l'organisation en tile, on est certain que les 4 texels seront dans la même tile, sauf s'ils ont le malheur d'être tout au bord d'une tile. Ce dernier cas est assez rare, et il l'est d'autant plus que les tiles sont grandes. Enfin, un dernier avantage est que les tiles sont généralement assez petites pour tenir tout entier dans une ligne de cache. Le cache de texture est donc utilisé à merveille, ce qui rend les accès aux textures plus rapides.
====Les textures basées sur des ''z-order curves''====
Les formats de textures théoriquement optimaux utilisent une '''''Z-order curve''''', illustrée ci-dessous. L'idée est de découper la texture en quatre rectangles identiques, et de stocker ceux-ci les uns à la suite des autres. L'intérieur de ces rectangles est lui aussi découpé en quatre rectangles, et ainsi de suite. Au final, l'ordre des pixels en mémoire est celui illustré ci-dessous.
[[File:Z-CURVE.svg|centre|vignette|upright=2|Construction d'une ''Z-order curve''.]]
Les texels sont stockés les uns à la suite des autres dans la mémoire, en suivant l'ordre donnée par la ''Z-order curve''. Le calcul d'adresse calcule la position du texel en mémoire, par rapport au début de la texture, et ajoute l'adresse du début de la texture. Mais tout le défi est de calculer la position d'un texel en mémoire, à partir des coordonnées x,y. Le calcul peut sembler très compliqué, mais il n'en est rien. Le calcul demande juste de regarder les bits des deux coordonnées et de les combiner d'une manière particulièrement simple. Il suffit de placer le bit de poids fort de la coordonnée x, suivi de celui de la coordonnée y, et de faire ainsi de suite en passant aux bits suivants.
[[File:Zcurve45bits.png|centre|vignette|upright=1.5|Calcul de la position d'un élément dans une ''Z-order curve'' à partir des coordonnées x et y.]]
L'avantage d'une telle organisation est que la textures est découpées en ''tiles'' rectangulaires d'une certaine taille, elles-mêmes découpées en ''tiles'' plus petites, etc. Et il se trouve que cette organisation est parfaite pour le cache de texture. L'idéal pour le cache de texture est de charger une ''tile'' complète dans le cache de textures. Quand on accède à un texel, on s'assure que la ''tile'' complète soit chargée. Mais cela demande de connaitre à l'avance la taille d'une ''tile''. Les formats de texture fournissent généralement une ''tile'' carré de 4 pixels de côté, mais cela donnerait un cache trop petit pour être vraiment utile. Avec cette méthode, on s'assure qu'il y ait une ''tile'' avec la taille optimale. Les ''tiles'' étant découpées en ''tiles'' plus petites, elles-mêmes découpées, et ainsi de suite, on s'assure que la texture est découpées en ''tiles'' de taille variées. Il y aura au moins une ''tile'' qui rentrera tout pile dans le cache.
===Les techniques de rendu à textures multiples===
Nous venons de voir comment une texture est plaquée sur un objet 3D, ou une surface comme un sol. Pour résumer, le calcul de l'adresse d'un texel prend la position du texel par rapport au début de la texture, et ajoute l'adresse du début de la texture. L'adresse mémoire de la texture est connue au moment où le pilote de la carte graphique place la texture dans la mémoire vidéo, et cette information est transmise au matériel par l'intermédiaire du processeur de commande, puis passée aux processeurs de shaders et à l'unité de texture. Le tout est couplé à d'autres informations, la plus importante étant la ''taille de la texture en octets'', pour éviter de déborder lors des accès à la texture.
Néanmoins, il s'agit là du cas le plus simple. Certaines techniques de rendu demandent de choisir la texture à plaquer parmi un ensemble de plusieurs textures. Les techniques en question sont assez variées et n'ont pas grand chose en commun. Les plus connues sont le ''mip-mapping'', le ''cube-mapping'' et les textures virtuelles. Le ''mip-mapping'' sert à filtrer les textures, chose qu'on expliquera plus tard, le ''cube-mapping'' sert à simuler des réflexions sur un objet en plaquant une texture de l'environnement dessus, les textures virtuelles sont une optimisation pour les textures des terrains de grande taille. Mais malgré leurs différences, elles demandent de choisir quelle texture plaquer entre plusieurs textures de base. En clair, l'adresse de base de la texture varie selon la situation. Voyons-les dans le détail.
==L'implémentation matérielle du placage de textures==
Pour résumer, la lecture d'un texel demande d'effectuer plusieurs étapes. Dans le cas le plus simple, sans ''mip-mapping'' ou ''cubemapping'', on doit effectuer les étapes suivantes :
* Il faut d'abord normaliser les coordonnées de texture pour qu'elles tombent dans l'intervalle [0,1] en fonction du mode d'adressage désiré.
* Ensuite, les coordonnées u,v doivent être converties en coordonnées entières, ce qui demande une multiplication flottante.
* Enfin, l'adresse finale est calculée à partir des coordonnées entières et en ajoutant l'adresse de base de la texture (et éventuellement avec d'autres calculs arithmétiques suivant le format de la texture).
Tout cela pourrait être fait par le pixel shaders, mais cela implique beaucoup de calculs répétitifs et d'opérations arithmétiques assez lourdes, avec des multiplications flottantes, des additions et des multiplications entières, etc. Faire faire tous ces calculs par les shaders serait couteux en performance, sans compter que les shaders deviendraient plus gros et que cela aurait des conséquences sur le cache d'instruction. De plus, certaines de ces étapes peuvent se faire en parallèle, comme les deux premières, ce qui colle mal avec l'aspect sériel des shaders.
Aussi, les processeurs de shaders incorporent une unité de calcul d'adresse spéciale pour faire ces calculs directement en matériel. L'unité de calcul en question est dans l'unité de texture. Cette dernière contient donc au minimum deux circuits : un circuit de calcul d'adresse, et un circuit d'accès à la mémoire. Toute la difficulté tient dans le calcul d'adresse, plus que dans le circuit de lecture. Le calcul d'adresse est conceptuellement réalisé en deux étapes. La première étape qui transforme les coordonnées u,v en coordonnées x,y qui donne le numéro de la ligne et de la colonne du texel dans la texture. La seconde étape prend ces deux coordonnées x,y, l'adresse de la texture, et détermine l'adresse de la tile à lire.
[[File:Unité de texture simple.png|centre|vignette|upright=2|Unité de texture simple]]
===La gestion des accès mémoire===
Enfin, l'unité de texture doit tenir compte du fait que la mémoire vidéo met du temps à lire une texture. En théorie, l'unité de texture ne devrait pas accepter de nouvelle demande de lecture tant que celle en cours n'est pas terminée. Mais faire ainsi demanderait de bloquer tout le pipeline, de l'''input assembler'' au unités de''shaders'', ce qui est tout sauf pratique et nuirait grandement aux performances.
Une solution alternative consiste à mettre en attente les demandes de lectures de texture pendant que la mémoire est occupée. La manière la plus simple d'implémenter des accès mémoire multiples est de les mettre en attente dans une petite mémoire FIFO. Cela implique que les accès mémoire s’exécutent dans l'ordre demandé par le ''shader'' et/ou l'unité de rastérisation, il n'y a pas de réorganisation des accès mémoire ou d’exécution dans le désordre des accès mémoire.
[[File:Texture prefetching.png|centre|vignette|upright=1.5|Accès mémoire simultanés.]]
Évidemment, quand la mémoire FIFO est pleine, le pipeline est alors totalement bloqué. Le rasteriser est prévenu que l'unité de texture ne peut pas accepter de nouvelle lecture de texture. En pratique, la FIFO est généralement d'une taille respectable et permet de mettre en attente beaucoup de demandes de lecture de texture. Il faut de plus noter qu'il y a une FIFO par processeur de ''shader'' sur les cartes graphiques modernes. Quand elle est pleine, le processeur cesse d'exécuter de nouveaux accès mémoire, mais peut continuer à exécuter des ''shaders'' dans les autres unités de calcul, pas besoin de bloquer complétement le pipeline.
===L'intégration du cache de textures===
Il faut noter que les unités de texture incorporent aussi un cache de texture, voire plusieurs. L'intégration des caches de texture avec la mémoire FIFO précédente est quelque peu compliqué, car il faut garantir que les lectures de texture se fassent dans le bon ordre. On ne peut pas exécuter une lecture dans le cache alors que des lectures précédentes sont en attente de lecture en mémoire vidéo. Et cela pose un gros problème : une lecture dans le cache de texture prend quelques dizaines de cycles d'horloge, alors qu'une lecture en mémoire vidéo en prend facilement 400 à 800 cycles, parfois plus. Et cela fait que l'ordre des accès mémoire peut s'inverser.
Prenons par exemple un accès au cache précédé et suivi par deux accès en mémoire vidéo. Le premier démarre au cycle 1, et se termine au cycle numéro 400. L'accès au cache commence au cycle 2 et se termine 20 cycles après, au cycle numéro 22. En clair, la lecture dans le cache s'est terminée avant l'accès mémoire qui le précède. Les textures ne sont donc plus lues dans l'ordre. Et il faut trouver une solution pour éviter cela.
La solution est de retarder les lectures dans le cache tant que tous les accès précédents ne sont pas terminés. Mais pour retarder les lectures en question, il faut d'abord savoir si la lecture atterrit dans le cache ou non, ce qui demande d'accéder au cache. On fait face à un dilemme : on veut retarder les accès au cache, mais les différencier des lectures déclenchant des accès mémoire demande d'accéder au cache en premier lieu. La solution est décrite dans l'article "Prefetching in a Texture Cache Architecture" par Igehy et ses collègues. Elle se base sur deux idées combinées ensemble.
La première idée est de séparer l'accès au cache en deux : une étape qui vérifie si les texels à lire sont dans le cache, et une étape qui accède aux données dans le cache lui-même. Un cache de texture est donc composé de deux circuits principaux. Le premier vérifie la présence des texels dans le cache. Il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Ensuite, en plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
La seconde idée est de retarder l'accès au cache entre les deux phases. La première étape d'un accès mémoire vérifie si la donnée est dans le cache ou non. Puis, on retarde la lecture des données, pour attendre que toutes les lectures précédentes soient terminées. Et enfin, troisième étape : la lecture des texels dans la mémoire cache proprement dite. Les accès mémoire passant par la mémoire vidéo se font de la même manière, à une différence près : la lecture dans le cache est remplacée par la lecture en mémoire vidéo. Tout démarre avec une demande à l'unité de tags, qui vérifie si le texel est dans le cache ou non. Puis on retarde l'accès tant que la mémoire vidéo est occupée, puis on effectue la lecture en mémoire vidéo.
Si ce n'est pas le cas, l'accès mémoire est envoyé à la mémoire vidéo comme précédemment, à savoir qu'il est mis en attente dans une mémoire FIFO, puis envoyé à la mémoire vidéo dès que celle-ci est libre. Mais en sortie de la mémoire, la donnée lue est envoyée dans le cache de texture, par dans l'unité de filtrage. Pour savoir où placer la donnée lue, l'unité de tag a réservé une ligne de cache précise, une adresse bien précise. L'adresse en question est disponible en lisant une autre mémoire FIFO, qui a mis en attente l'adresse en question, en attendant que l'accès mémoire se termine. La donnée est alors écrite dans le cache, puis lue par l'unité de filtrage de textures.
Pour une lecture dans le cache, le déroulement est similaire, mais sans le passage par la mémoire. La lecture fait une demande à l'unité de tag, et celle-ci répond que la donnée est bien dans le cache. Elle place alors l'adresse à lire dans la file d'attente. Une fois que les accès mémoire précédents sont terminés, l'adresse sort de la file d'attente et est envoyée à la mémoire de données. La lecture s'effectue, les texels sont envoyés à l'unité de filtrage de textures. La seule différence avec un ''phased cache'' normal est l'insertion de l'adresse à lire dans une FIFO qui vise à mettre en attente
[[File:Unité de texture avec un cache de texture.png|centre|vignette|upright=2.0|Unité de texture avec un cache de texture]]
Pour résumer, l'implémentation précédente garantit une exécution des lectures dans leur ordre d'arrivée. Et pour cela, elle retarde les lectures dans le cache tant que les lectures en mémoire précédentes ne sont pas terminées. L'accès au cache est plus rapide que l'accès en mémoire vidéo, mais le retard ajouté pour garantir l'ordre des lectures fait que le temps d'accès est très long.
==Le mip-mapping==
Le '''mip-mapping''' a pour but de légèrement améliorer les graphismes des objets lointains, tout en rendant les calculs de texture plus rapides. Formellement, le ''mip-mapping'' est une technique de filtrage de texture, mais nous l'abordons maintenant car elle est surtout liée au calcul d'adresse. Les unités de texture ont des circuits de filtrage de texture séparés des circuits de ''mip-mapping'' et de calcul d'adresse, d'où le fait que nous en parlons séparément.
Le problème résolu par le ''mip-mapping'' est le rendu des textures lointaines. Si une texture est plaquée sur un objet lointain, une bonne partie des détails est invisible pour l'utilisateur. Un pixel de l'écran est associé à plusieurs texels. Idéalement, la carte graphiques devrait lire tous ces texels et en faire une sorte de moyenne pondérée, pour calculer la couleur finale du pixel. Mais dans les faits, ce serait très gourmand et compliqué à implémenter en hardware. Une solution serait de ne garder que quelque texels, mais cela a tendance à créer des artefacts visuels (les textures affichées ont tendance à pixeliser). Le ''mip-mapping'' permet de réduire ces deux problèmes en même temps en précalculant cette moyenne pondérée pour des distances prédéfinies.
L'idée est d'utiliser plusieurs exemplaires d'une même texture à des résolutions différentes, chaque exemplaire étant adapté à une certaine distance. Par exemple, une texture sera stocké avec un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre de 128 * 128 et ainsi de suite jusqu’à un dernier exemplaire de 32 * 32 pixel. Chaque exemplaire correspond à un '''niveau de détail''', aussi appelé ''Level Of Detail'' (abrévié en LOD). La résolution utilisée diminue d'autant plus que l'objet est situé loin de la caméra. Les objets proches seront rendus avec la texture 512*512, ceux plus lointains seront rendus avec la texture de résolution 256*256, les textures 128*128 seront utilisées encore plus loin, et ainsi de suite jusqu'aux objets les plus lointains qui sont rendus avec la texture la plus petite de 32*32.
[[File:MipMap Example STS101.jpg|centre|vignette|upright=2|Exemples de mip-maps.]]
Le ''mip-mapping'' améliore grandement la qualité d'image. L'image d'exemple ci-dessous le montre assez bien.
[[File:Mipmapping example.png|centre|vignette|upright=2|Exemple de mipmapping.]]
Pour faciliter les calculs d'adresse, les LOD d'une même texture sont stockées les uns après les autres en mémoire (dans un tableau, comme diraient les programmeurs). Ainsi, pas besoin de se souvenir de la position en mémoire de chaque LOD : l'adresse de la texture de base, et quelques astuces arithmétiques suffisent. Prenons le cas où la texture de base a une taille L. le premier exemplaire est à l'adresse 0, le second niveau de détail est à l'adresse L, le troisième à l'adresse L + L/4, le suivant à l'adresse L + L/4 + L/16, et ainsi de suite. Le calcul d'adresse demande juste connaître le niveau de détails souhaité et l'adresse de base de la texture. Le niveau de détail voulu est calculé par les pixel shaders, en fonction de la coordonnée de profondeur du pixel à traiter.
Évidemment, cette technique consomme de la mémoire vidéo, vu que chaque texture est dupliquée en plusieurs exemplaires, en plusieurs LOD. Dans le détail, la technique du mip-mapping prend au maximum 33% de mémoire en plus (sans compression). Cela vient du fait qu'en prenant une texture dexu fois plus petite, elle prend 4 fois moins de mémoire : 2 fois moins de pixels en largeur, et 2 fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des LODs, texture de base comprise, prendra X + (X/4) + (X/16) + (X/256) + … Un petit calcul de limite donne 4/3 * X, soit 33% de plus.
===L'implémentation du mip-mapping dans l'unité de texture===
Le ''mip-mapping'' est lui aussi pris en charge par l'unité de calcul d'adresse, car cette technique change l'adresse de base de la texture. La gestion du ''mip-mapping'' est cependant assez complexe. Il est possible de laisser le pixel shader calculer quel niveau de détail utiliser, en fonction de la coordonnée de profondeur z du pixel à afficher. La carte graphique détermine alors automatiquement quelle texture lire, quel niveau de détail, automatiquement. Elle détermine aussi la bonne résolution pour la texture, qui est égal à la résolution de la texture de base, divisée par le niveau de détail. Pour résumer, le niveau de détail est envoyé aux unités de texture, qui s'occupent de calculer l'adresse de base et la résolution adéquates. Quelques calculs arithmétiques simples, donc, qui s'implémentent facilement avec quelques circuits.
Mais une autre méthode laisse la carte graphique déterminer le niveau de détail par elle-même. Dans ce cas, cela demande, outre les deux coordonnées de texture, de calculer la dérivée de ces deux coordonnées dans le sens horizontal et vertical, ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Les quatre dérivées sont les suivantes :
: <math>\frac{du}{dx}</math>, <math>\frac{dv}{dx}</math>, <math>\frac{du}{dy}</math>, <math>\frac{dv}{dy}</math>
Un bon moyen pour obtenir les dérivées demande de regrouper les pixels par groupes de 4 et de faire la différence entre leurs coordonnées de texture respectives. On peut calculer les deux dérivées horizontales en comparant les deux pixels sur la même ligne, et les deux dérivées verticales en comparant les deux pixels sur la même colonne. Mais cela demande de rastériser les pixels par groupes de 4, par ''quads''. Et c'est ce qui est fait sur les cartes graphiques actuelles, qui rastérisent des groupes de 4 pixels à la fois.
[[File:Texture sampler unit with mipmapping.png|centre|vignette|upright=2.0|Unité de texture avec mipmapping.]]
Malheureusement, le calcul exact utilisé pour le choix de la mip-map dépend du GPU considéré et peu de chose est connu quant à ces algorithmes. Il est possible d'inférer le comportement à partir d'observations, mais guère plus. Pour ceux qui veulent en savoir plus, je conseille la lecture de cet article de blog :
* [https://pema.dev/2025/05/09/mipmaps-too-much-detail/ Mipmap selection in too much detail]
==Le filtrage de textures==
Plaquer des textures sans autre forme de procès ne suffit pas à garantir des graphismes d'une qualité époustouflante. La raison est que les sommets et les texels ne tombent pas tout pile sur un pixel de l'écran : le sommet associé au texel peut être un petit peu trop en haut, ou trop à gauche, etc. Une explication plus concrète fait intervenir les coordonnées de texture. Souvenez-vous que lorsque l'on traduit une coordonnée de texture u,v en coordonnées x,y, on obtient un résultat qui ne tombe pas forcément juste. Souvent, le résultat a une partie fractionnaire. Si celle-ci est non-nulle, cela signifie que le texel/sommet n'est pas situé exactement sur le pixel voulu et que celui-ci est situé à une certaine distance. Concrètement, le pixel tombe entre quatre texels, comme indiqué ci-dessous.
[[File:Filtrage texture.png|centre|vignette|upright=2.0|Position du pixel par rapport aux texels.]]
Pour résoudre ce problème, on doit utiliser différentes techniques d'interpolation, aussi appelées techniques de '''filtrage de texture''', qui visent à calculer la couleur du pixel final en fonction des texels qui l'entourent. Il existe de nombreux types de filtrage de textures, qu'il s'agisse du filtrage linéaire, bilinéaire, trilinéaire, anisotropique et bien d'autres.
Tous ont besoin d'avoir certaines informations qui sont généralement fournies par les circuits de calcul d'adresse. La première est clairement la partie fractionnaire des coordonnées x,y. La seconde est la dérivée de ces deux coordonnées dans le sens horizontal et vertical., ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Toujours est-il que le filtrage de texture est une opération assez lourde, qui demande beaucoup de calculs arithmétiques. On pourrait en théorie le faire dans les pixels shaders, mais le cout en performance serait absolument insoutenable. Aussi, les cartes graphiques intègrent toutes un circuit dédié au filtrage de texture, le ''texture sampler''. Même les plus anciennes cartes graphiques incorporent une unité de filtrage de texture, ce qui nous montre à quel point cette opération est importante.
[[File:Texture unit.png|centre|vignette|upright=2.0|Unité de texture.]]
On peut configurer la carte graphique de manière à ce qu'elle fasse soit du filtrage bilinéaire, soit du filtrage trilinéaire, on peut configurer le niveau de filtrage anisotropique, etc. Cela peut se faire dans les options de la carte graphique, mais cela peut aussi être géré par l'application. La majorité des jeux vidéos permettent de régler cela dans les options. Ces réglages ne concernent pas la texture elle-même, mais plutôt la manière dont l'unité de texture doit fonctionner. Ces réglages sur l''''état de l'unité de texture''' sont mémorisés quelque part, soit dans l'unité de texture elle-même, soit fournies avec la ressource de texture elle-même, tout dépend de la carte graphique. Certaines cartes graphiques mémorisent ces réglages dans les unités de texture ou dans le processeur de commande, et tout changement demande alors de réinitialiser l'état des unités de texture, ce qui prend un peu de temps. D'autres placent ces réglages dans les ressources de texture elles-mêmes, ce qui rend les modifications de configuration plus rapides, mais demande plus de circuits. D'autres cartes graphiques mélangent les deux options, certains réglages étant globaux, d'autres transmis avec la texture. Bref, difficile de faire des généralités, tout dépend du matériel et le pilote de la carte graphique cache tout cela sous le tapis.
Maintenant que cela est dit, voyons quelles sont les différentes méthodes de filtrage de texture et comment la carte graphique fait pour les calculer.
===Le filtrage au plus proche===
La méthode de filtrage la plus simple consiste à colorier avec le texel le plus proche. Cela revient tout simplement à ne pas tenir compte de la partie fractionnaire des coordonnées x,y, ce qui est très simple à implémenter en matériel. C'est ce que l'on appelle le '''filtrage au plus proche''', aussi appelé ''nearest filtering''.
Autant être franc, le résultat est assez pixelisé et peu agréable à l’œil. Par contre, le résultat est très rapide à calculer, vu qu'il ne demande aucun calcul à proprement parler. Elle ne fait pas appel à la parti fractionnaire des coordonnées entières de texture, ni aux dérivées de ces coordonnées. On peut combiner cette technique avec le mip-mapping, ce qui donne un résultat bien meilleur, bien que loin d'être satisfaisant. Au passage, toutes les techniques de filtrage de texture peuvent se combiner avec du mip-mapping, certaines ne pouvant pas faire sans.
[[File:Interpolation-nearest.svg|centre|vignette|Filtrage de texture au plus proche.]]
===Le filtrage linéaire===
Le filtrage le plus simple est le '''filtrage linéaire'''. Il effectue une interpolation linéaire entre deux mip-maps, deux niveaux de détails. Pour comprendre l'idée, nous allons prendre une situation très simple, avec une texture carrée de 512 texels de côté. Le mip-mapping crée plusieurs textures : une de 256 texels de côté, une de 128 texels, une de 64, etc. Maintenant, la texture est sur un objet à une certaine distance de l'écran, vu de face. Le résultat est qu'elle correspond à l'écran à un carré de 300 pixels de côté (pas d'erreur : pixels, pas texels). Dans ce cas, la texture se trouve entre deux mip-maps : celle de 512 pixels de côté, celle de 256. Laquelle choisir ? Le filtrage au plus proche prend la texture de 512 pixels de côté. Le filtrage linéaire lui, fait autrement.
Vu que la texture est entre deux mip-maps, l'idée est de prendre le texel au plus proche dans chaque texture et de faire une sorte de moyenne appelée l'interpolation linéaire. L'interpolation par du principe que la couleur varie entre les deux texels en suivant une fonction affine, illustrée ci-dessous. Ce ne serait évidemment pas le cas dans le monde réel, mais on supposer cela donne une bonne approximation de ce à quoi ressemblerait une texture à plus haute résolution. On peut alors calculer la couleur du pixel par une simple moyenne pondérée par la distance. Le résultat est que les transitions entre deux niveaux de détails sont plus lisses, moins abruptes.
[[File:Lin interp -é.png|centre|vignette|upright=2.0|Interpolation linéaire.]]
===Le filtrage bilinéaire===
Le filtrage bilinéaire effectue une sorte de moyenne pondérée des quatre texels les plus proches du pixel à afficher. Pour cela, rappelez-vous ce qui a été dit plus haut : les coordonnées x,y d'un pixel ont une partie entière et une partie fractionnaire. Le filtrage au plus proche élimine les parties fractionnaires, ce qui donne une coordonnée x,y. Avec le filtrage bilinéaire, on prend les texels de coordonnées (x,y) ; (x+1,y) ; (x,y+1) ; (x+1,y+1), le pixel étant entre ces 4 texels.
Mais le filtrage ne fait pas qu'une simple moyenne, il prend en compte les parties fractionnaires pour faire la moyenne. En effet, le pixel n'est pas au milieu du carré de texel, il est quelque part mais est souvent plus proche d'un texel que des autres. Et il faut donc pondérer la moyenne par les distances aux 4 texels. Pour cela, la moyenne est calculée à partir d'interpolations linéaires. Avec 4 pixels, nous allons devoir calculer la couleur de deux points intermédiaires. La couleur de ces deux points se calcule par interpolation linéaire, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.
[[File:Bilin3.png|centre|vignette|upright=2|Filtrage bilinéaire de texture.]]
Le circuit qui permet de faire l'interpolation bilinéaire est particulièrement simple. On trouve un circuit de chaque pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu, et un pour la transparence. Chacun de ces circuit est composé de sous-circuits chargés d'effectuer une interpolation linéaire, reliés comme suit.
[[File:Texture sampler unit.png|centre|vignette|Unité de filtrage bilinéaire.]]
Vous noterez que le filtrage bilinéaire accède à 4 pixels en même temps. Fort heureusement, les textures sont stockées de manière à ce qu'on puisse charger les 4 pixels en une fois, comme on l'a vu plus haut. Le filtrage bilinéaire a de fortes chances que les 4 pixels filtrés soient dans la même ''tile'', la seule exception étant quand ils sont tout juste sur le bord d'une ''tile''.
: La console de jeu Nintendo 64 n'utilise que trois pixels au lieu de quatre dans son interpolation bilinéaire, qui en devient une interpolation quasi-bilinéaire. La raison derrière ce choix est une question de performances, comme beaucoup de décisions de ce genre. Le résultat est un rendu imparfait de certaines textures.
===Le filtrage trilinéaire===
Avec le filtrage bilinéaire, des discontinuités apparaissent sur certaines surfaces. Par exemple, pensez à une texture de sol : elle est appliquée plusieurs fois sur toute la surface du sol. A une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512*512 à 256*256, ce qui est visible pour un joueur attentif. De telles transitions sont lissées grâce au filtrage linéaire, il n'y a plus qu'à le combiner avec le filtrage bilinéaire. Rien d’incompatible : le premier filtre l'intérieur d'une mip-map, le second combine deux mip-maps.
Le filtrage trilinéaire prend les deux mip-maps les plus proches, fait un filtrage bilinéaire avec chacune, puis fait une « une moyenne » pondérée entre les deux résultats. Le circuit de filtrage trilinéaire existe en plusieurs versions. La plus simple, illustrée ci-dessous, effectue deux filtrages bilinéaires en parallèle, dans deux circuits séparés, puis combine leurs résultats avec un circuit d'interpolation linéaire. Mais ce circuit nécessite de charger 8 texels simultanément. Qui plus est, ces 8 texels ne sont pas consécutifs en mémoire, car ils sont dans deux niveaux de détails/mip-maps différents.
[[File:Parallel trilinear filtering.png|centre|vignette|upright=2.0|Unité de filtrage trilinéaire parallèle.]]
Vu qu'on lit des texels dans deux mip-maps, les texels sont lus en deux fois : 4 texels provenant de la première mip-map, suivis par les 4 texels de l'autre mip-map. Les 4 premiers texels doivent donc être mis en attente dans des registres, en attendant que les 4 autres arrivent. Une amélioration du circuit précédent gère cela en ajoutant des registres. Il lit les 4 premiers texels, les filtre avec une interpolation bilinéaire, et mémorise le résultat dans un registre. Puis, il lit les 4 autres texels, les filtre, et met le résultat dans un second registre. A ce moment là, un circuit d'interpolation linéaire finit le travail. On économise donc un circuit d'interpolation bilinéaire, sans que les performances soient trop impactées.
[[File:Filtrage trilineaire.png|centre|vignette|upright=1.0|Unité de filtrage trilineaire série.]]
Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le temps où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. À chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients, et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.
===Le filtrage anisotrope===
D'autres artefacts peuvent survenir lors de l'application d'une texture, la perspective pouvant déformer les textures et entraîner l'apparition de flou. La raison à cela est que les techniques de filtrage de texture précédentes partent du principe que la texture est vue de face. Prenez une texture carrée, par exemple. Vue de face, elle ressemble à un carré sur l'écran. Mais tournez la caméra, de manière à voir la texture de biais, avec un angle, et vous verrez que la forme de la texture sur l'écran est un trapèze, pas un carré. Cette déformation liée à la perspective n'est pas prise en compte par les méthodes de filtrage de texture précédentes. Pour le dire autrement, les techniques de filtrage précédentes partent du principe que les 4 texels qui entourent un pixel forment un carré, ce qui est vrai si la texture est vue de face, sans angle, mais ne l'est pas si la texture n'est pas perpendiculaire à l'axe de la caméra. Du point de vue de la caméra, les 4 texels forment un trapèze d'autant moins proche d'un carré que l'angle est grand.
Pour corriger cela, les chercheurs ont inventé le '''filtrage anisotrope'''. En fait, je devrais plutôt dire : LES filtrages anisotropes. Il en existe un grand nombre, dont certains ne sont pas utilisés dans les cartes graphiques actuelles, soit car ils trop gourmand en accès mémoires et en calculs pour être efficaces, soit car ils ne sont pas pratiques à mettre en œuvre. Il est très difficile de savoir quelles sont les techniques de filtrage de texture utilisées par les cartes graphiques, qu'elles soient récentes ou anciennes. Beaucoup de ces technologies sont brevetées ou gardées secrètes, et il faudrait vraiment creuser les brevets déposés par les fabricants de GPU pour en savoir plus. Les algorithmes en question seraient de plus difficiles à comprendre, les méthodes mathématiques cachées derrière ces méthodes de filtrage n'étant pas des plus simple.
[[File:Anisotropic filtering en.png|centre|vignette|upright=2|Exemple de filtrage anisotrope.]]
==La compression de textures==
Les textures les plus grosses peuvent aller jusqu'au mébioctet, ce qui est beaucoup. Pour limiter la casse, les textures sont compressées. La '''compression de texture''' réduit la taille des textures, ce qui peut se faire avec ou sans perte de qualité. Elle entraîne souvent une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande. Mais il s'agit là d'une technique très simple, beaucoup plus simple que les techniques que nous allons voir dans cette section. Nous allons voir quelque algorithmes de compression de textures de complexité intermédiaire, mais n'allons pas voir l'état de l'art. Il existe des formats de texture plus récents que ceux qui nous allons aborder, comme l{{'}}''Ericsson Texture Compression'' ou l{{'}}''Adaptive Scalable Texture Compression'', plus complexes et plus efficaces.
Notons que les textures sont compressées dans les fichiers du jeu, mais aussi en mémoire vidéo. Les textures sont décompressées lors de la lecture. Pour cela, la carte graphique contient alors un circuit, capable de décompresser les textures lorsqu'on les lit en mémoire vidéo. Les cartes graphiques supportent un grand nombre de formats de textures, au niveau du circuit de décompression. Du fait que les textures sont décompressées à la volée, les techniques de compression utilisées sont assez particulières. La carte graphique ne peut pas décompresser une texture entière avant de pouvoir l'utiliser dans un ''pixel shader''. A la place, on doit pouvoir lire un morceau de texture, et le décompresser à la volée. On ne peut utiliser les méthodes de compression du JPEG, ou d'autres formats de compression d'image. Ces dernières ne permettent pas de décompresser une image morceau par morceau.
Pour permettre une décompression/compression à la volée, les textures sont des textures tilées, généralement découpées en tiles de 4 * 4 texels. Les ''tiles'' sont compressées indépendamment les unes des autres. Et surtout, avec ou sans compression, la position des tiles en mémoire ne change pas. On trouve toujours une tile tous les T octets, peu importe que la tile soit compressée ou non. Par contre, une tile compressée n'occupera pas T octets, mais moins, là où une tile compressée occupera la totalité des T octets. En clair, compresser une tile fait qu'il y a des vides entre deux tiles dans al mémoire vidéo, mais ne change rien à leur place en mémoire vidéo qui est prédéterminée, peu importe que la texture soit compressée ou non. L'intérêt de la compression de textures n'est pas de réduire la taille de la texture en mémoire vidéo, mais de réduire la quantité de données à lire/écrire en mémoire vidéo. Au lieu de lire T octets pour une tile non-compressée, on pourra en lire moins.
===La palette indicée et la technique de ''Vector quantization''===
La technique de compression des textures la plus simple est celle de la '''palette indicée''', que l'on a entraperçue dans le chapitre sur les cartes d'affichage. La technique de '''''vector quantization''''' peut être vue comme une amélioration de la palette, qui travaille non pas sur des texels, mais sur des ''tiles''. À l'intérieur de la carte graphique, on trouve une table qui stocke toutes les ''tiles'' possibles. Chaque ''tile'' se voit attribuer un numéro, et la texture sera composé d'une suite de ces numéros. Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l’embarqué utilisent ce genre de compression.
===Les algorithmes de ''Block Truncation coding''===
La première technique de compression élaborée est celle du '''''Block Truncation Coding''''', qui ne marche que pour les images en niveaux de gris. Le BTC ne mémorise que deux niveaux de gris par ''tile'', que nous appellerons couleur 1 et couleur 2, les deux niveaux de gris n'étant pas le même d'une ''tile'' à l'autre. Chaque pixel d'une ''tile'' est obligatoirement colorié avec un de ces niveaux de gris. Pour chaque pixel d'une ''tile'', on mémorise sa couleur avec un bit : 0 pour couleur 1, et 1 pour couleur 2. Chaque ''tile'' est donc codée par deux entiers, qui codent chacun un niveau de gris, et une suite de bits pour les pixels proprement dit. Le circuit de décompression est alors vraiment très simple, comme illustré ci-dessous.
[[File:Block Truncation coding.jpg|centre|vignette|upright=2.0|Block Truncation coding.]]
La technique du BTC peut être appliquée non pas du des niveaux de gris, mais pour chaque composante Rouge, Vert et Bleu. Dans ces conditions, chaque ''tile'' est séparée en trois sous-''tiles'' : un sous-bloc pour la composante verte, un autre pour le rouge, et un dernier pour le bleu. Cela prend donc trois fois plus de place en mémoire que le BTC pur, mais cela permet de gérer les images couleur.
===Le format de compression S3TC / DXTC===
L'algorithme de '''Color Cell Compression''', ou CCC, améliore le BTC pour qu'il gère des couleurs autre que des niveaux de gris. Ce CCC remplace les deux niveaux de gris par deux couleurs. Une ''tile'' est donc codée avec un entier 32 bits par couleur, et une suite de bits pour les pixels. Le circuit de décompression est identique à celui utilisé pour le BTC.
[[File:Color Cell Compression.jpg|centre|vignette|Color Cell Compression.]]
[[File:Dxt1-memory-layout.png|vignette|Dxt1 et ''color cell compression''.]]
Le format de compression de texture utilisé de base par Direct X, le DXTC, est une version amliorée de l'algorithme précédent. Il est décliné en plusieurs versions : DXTC1, DXTC2, etc. La première version du DXTC est une sorte d'amélioration du CCC : il ajoute une gestion minimale de transparence, et découpe la texture à compresser en ''tiles'' de 4 pixels de côté. La différence, c'est que la couleur finale d'un texel est un mélange des deux couleurs attribuée au bloc. Pour indiquer comment faire ce mélange, on trouve deux bits de contrôle par texel.
Si jamais la couleur 1 < couleur2, ces deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (2 * Couleur1 + Couleur2) / 3
* 11 = (Couleur1 + 2 * Couleur2) / 3
Sinon, les deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (Couleur1 + Couleur2) / 2
* 11 = Transparent
[[File:DXTC.jpg|centre|vignette|DXTC.]]
Le circuit de décompression du DXTC ressemble alors à ceci :
[[File:Circuit de décompression du DXTC.jpg|centre|vignette|upright=2.0|Circuit de décompression du DXTC.]]
===Les format DXTC 2, 3, 4 et 5 : l'ajout de la transparence===
Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3, lui-même replacé par le DXT4 et par le DXT5. Dans le DXT3, la transparence fait son apparition. Pour cela, on ajoute 64 bits par ''tile'' pour stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1.
[[File:Dxt23-memory-layout.png|centre|vignette|Dxt 2 et 3.]]
Dans le DXT4 et le DXT5, la méthode utilisée pour compresser les couleurs l'est aussi pour les valeurs de transparence. L'information de transparence est stockée par un en-tête contenant deux valeurs de transparence, le tout suivi d'une matrice qui attribue trois bits à chaque texel. En fonction de la valeur des trois bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1.
[[File:Dxt45-memory-layout.png|centre|vignette|Dxt 4 et 5.]]
===Le format de compression PVRTC===
Passons maintenant à un format de compression de texture un peu moins connu, mais pourtant omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle travaille surtout dans les cartes graphiques embarquées. Ses cartes se trouvent notamment dans l'ipad, l'iPhone, et bien d'autres smartphones actuels.
Avec le PVRTC, les textures sont encore une fois découpées en ''tiles'' de 4 texels par 4, mais la ressemblance avec le DXTC s’arrête là. Chacque ''tile'' est codée avec :
* une couleur codée sur 16 bits ;
* une couleur codée sur 15 bits ;
* 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
* et un bit de modulation, qui permet de configurer l’interprétation des bits de mélange.
Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4*4.
==Annexe : le ''normal-mapping'' hardware==
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, parlons un peu du ''normal mapping'' et de son implémentation dans les unités de texture. Pour rappel, 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. Et pour comprendre quel est le rapport avec les textures, nous allons devoir faire quelques rappels sur l'éclairage par pixel.
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
L'éclairage se fait en utilisant de nombreux calculs, qu'on a détaillé dans le chapitre sur les bases du rendu 3D. Ceux-ci utilisent la normale d'un sommet, à savoir un vecteur orienté à la verticale de la surface. En théorie, il y a une normale par sommet, ce qui fait que les calculs d'éclairage doivent se faire au niveau géométrique, avant la rastérisation. Mais pour obtenir un éclairage de meilleure qualité, il y a des techniques qui calcule l'éclairage d'une scène 3D pixel par pixel.
Une première technique d'éclairage par pixel interpole les normales lors de l'étape de rastérisation. On obtient alors un éclairage de Phong. Une autre solution est celle du ''normal mapping''. Le '''''normal mapping''''' précalcule les normales d'une surface dans une texture, appelée la ''normal map''. L'éclairage est alors réalisé par un pixel shader, qui lit les normales depuis cette texture et fait les calculs d'éclairage avec.
===L'usage de textures non-compressées pour les ''normal maps''===
La ''normal map'' est une texture, donc. Mais ce n'est pas une texture comme une autre. Elle mémorise un vecteur pour chaque texel, pas une couleur. Il est possible d'utiliser les formats de textures non-compressés, même si cela ne donne pas de bons résultats. Après tout, un vecteur est codé par trois coordonnées x,y,z, il est possible de coder chacune avec un octet et de packager cela dans une texture RGB classique. La coordonnée x va dans la composante Rouge, la coordonnée Y dans la composante Bleu, et la coordonnée z dans la composante z.
Une première optimisation est de ne pas mémoriser les trois coordonnées, mais seulement deux d'entre elles et de calculer la troisième. En effet, les normales sont des vecteurs ''normalisés'', c'est à dire que leur longueur vaut 1, par construction. Vu que la taille est connue à l'avance, on peut en déduire la coordonnée z à partir des coordonnées x et y, avec l'usage du théorème de Pythagore. Par définition, <math>z^2 = 1 - x^2 - y^2</math>. Le calcul peut être fait dans le ''pixel shader'' sans problème.
: Pour être précis, il faut que les normales soient définies d'une certaine manière pour que ça marche. Les normales ne sont pas définies dans le même système de coordonnées que le modèle 3D, mais dans un autre système appelé l'''espace tangent''.
Notons que l'accès à la ''normal map'' se fait comme pour n'importe quelle texture : les texels sont lus en mémoire vidéo, puis le filtrage de texture est appliqué, et enfin le tout est envoyé au ''pixel shader''. En théorie, le filtrage effectue une sorte d'interpolation des normales automatique, ce qui permet de trouver la normale à un pixel précis, même s'il n'est pas sur un texel.
Les solutions précédentes demandent d'utiliser des textures non-compressées, qui utilisent beaucoup de mémoire vidéo. Utiliser des textures compressées pourrait sembler résoudre le problème. Mais l'usage de textures compressées marche très mal pour les ''normal maps'', elles ne permettent pas d'obtenir une qualité ou une compression suffisante. Aussi, des formats de texture dédiés aux ''normal maps'' ont été inventés.
===Le format de texture 3Dc===
Le problème est que le rendu final n'est pas très beau. Et les opérations de filtrage ne donnent pas de très bons résultats. Pour remédier à ces problèmes, des formats de texture spécialisés pour les ''normal map'' ont été inventés. Le premier d'entre eux est le '''3Dc''', aussi appelé BC5 pour ''Block Compression 5''. Il utilise un octet par texel, au lieu de trois avec une texture RGB normale non-compressée. Et surtout : il est géré en matériel, directement dans l'unité de texture, qui peut décompresser les textures 3Dc et les filtrer !
Avec le 3Dc, seules coordonnées sont mémorisées, la troisième est calculée, comme dit plus haut. Pour le reste, la compression de la ''normal map'' ressemble un peu à celle des textures. La ''normal map'' est découpée en blocs de 4 par 4 texels de côté, chacune étant encodée en tenant compte de certaines redondances. Dans un bloc, les coordonnées x sont dans un certain intervalle, allant de <math>x_{min}</math> à <math>x_{max}</math>, idem pour la coordonnée y qui est dans l'intervalle <math>y_{min}</math>, <math>y_{max}</math>. L'intervalle <math>x_{min}</math> à <math>x_{max}</math> est coupé en 8 parts égales, ce qui fait 8 valeurs possibles dans cet intervalle, chacune correspondant à une valeur x possible pour une normale du bloc. La normale est donc encodée en précisant quelle valeur est la bonne, en utilisant un indice de 3 bits pour cela. La valeur exacte de la coordonnée x se calcule à partir de l'indice comme suit :
: <math>x = x_{min} + \left( \frac{x_{min} - x_{min}}{8} \right) \times \text{indice} </math>
Pour encoder un bloc de 4 par 4 normales, il faut donc :
* Un octet pour chaque coordonnée <math>x_{min}</math>, <math>x_{max}</math>, <math>y_{min}</math>, <math>y_{max}</math>.
* 6 bits par normale : 3 pour encoder le décalage de la coordonnée x, 3 pour la coordonnée y.
Le tout permet d'encoder un bloc de 16 normales en seulement 128 bits, soit un octet par normale. Le ''pixel shader'' se débrouille pour décompresser un bloc. Il effectue notamment le calcul du dessus, pour retrouver la coordonnée x. Les calculs étant particulièrement simples, le cout en performance est très faible. Et ce d'autant plus qu'ils demandent juste de faire des additions et des multiplications entières, que l'unité scalaire peut faire en parallèle d'autres opérations plus gourmandes. Et le cout en calculs est de toute façon compensé par l'économie de mémoire et de bande passante lié à la compression : diviser la taille des données par trois, ca a un sacré impact !
==Annexe : les ''shadowmap'' hardware==
Les anciens GPU, notamment la Geforce FX, avaient des fonctionnalités spécifiques pour le calcul des ombres. Dans la plupart des jeux vidéos de l'époque, et même de maintenant, les ombres sont calculées avec la technique des ''shadowmap''. L'idée est assez simple sur le principe : un pixel est dans l'ombre quand il est invisible depuis une source de lumière.
L'idée est que le rendu est réalisé en plusieurs passes, avec une passe par source de lumière et une passe finale pour calculer l'image finale. Nous allons expliquer la technique avec une seule source de lumière, et allons utiliser l'exemple de la scène ci-dessous.
[[File:7fin.png|centre|vignette|Scène 3D d'exemple.]]
===La technique du ''shadowmapping''===
[[File:2shadowmap.png|vignette|Résultat de la première passe : ''shadowmap''..]]
La première passe rend l'image depuis le point de vue de la source de lumière. Cette première passe ne rend pas les couleurs de la scène, elle ne s'intéresse qu'à la profondeur des pixels. Le résultat est que l'image ne rend que le tampon de profondeur. Celui-ci est ensuite réutilisé comme texture pour la passe suivante. La texture en question est appelée la '''''shadownmap'''''.
La perspective utilisée, ainsi que le ''view frustrum'', dépend de la source de lumière. Pour une source de lumière qui émet un cône de lumière, le ''view frustrum'' de l'image rendue doit contenir tout le cone de lumière, et doit coller le plus possible à celui-ci. Pour une source directionnelle, comme le soleil, une perspective orthographique est utilisée.
La seconde passe rend l'image du point de vue de la caméra, pour rendre l'image finale. Elle rend l'image finale, qui est composée de pixels, chacun ayant une position à l'écran x,y, et une profondeur z. Les coordonnées sont transformées pour obtenir la position de ce pixel depuis le point de vue de la caméra. Une simple multiplication de matrice suffit, rien de bien compliqué, un shader peut le faire.
[[File:5failed.png|vignette|Résultat du test des comparaisons.]]
Après cette étape, on a alors les coordonnées x,y,z de ce pixel du point de vue de la caméra, et la ''shadowmap''. Il est alors possible d'accéder à la ''shadowmap'' au même endroit, à la même place que le pixel testé, aux mêmes coordonnées x,y. Si la profondeur du pixel est supérieure à celle de la shadowmap au même endroit, alors le pixel est situé derrière la surface visible, donc est dans l'ombre. Sinon, il n'est pas dans l'ombre. Le même procédé est répété sur chaque pixel de l'écran.
===Les optimisations hardware du ''shadowmapping''===
La technique des ''shadowmap'' demande donc de calculer une texture ''shadowmap'', puis de lire celle-ci et de faire des comparaisons de profondeur. Les GPU comme la Geforce FX intégraient du matériel dans les unités de texture pour faciliter ce travail. Les unités de texture pouvaient lire les ''shadowmap'', et faire la comparaison de profondeur toutes seules, elles avaient des circuits pour. Il suffisait de leur fournir le pixel à tester, ses coordonnées x,y,z, et l'adresse de la ''shadowmap''. Les unités de texture renvoyaient alors un résultat valant 0 ou 1 : 1 si le pixel est dans l'ombre, 0 sinon.
Elles pouvaient même effectuer du filtrage de texture sur les ''shadowmap''. Mais le filtrage était différent de celui utilisé sur les autres textures : moyenner des valeurs de profondeur ne marche pas bien. Elles utilisaient des techniques de filtrage différentes : elles faisaient les tests de comparaison, puis faisaient la moyenne des résultats. Ainsi, pour du filtrage bilinéaire, elles lisaient 4 texels dans la ''shadowmap'', puis faisaient 4 tests de comparaison, et moyennaient les 4 résultats.
==Annexe : le cube-mapping==
[[File:Cube mapped reflection example 2.JPG|vignette|Exemple de reflets environnementaux.]]
L''''environnement-mapping''' est une technique de calcul de divers effets graphiques liés à l'environnement, notamment des réflexions. L'idée est de plaquer une texture pré-calculée pour simuler l'effet de l'environnement sur une surface ou un objet 3D. Il en existe plusieurs versions différentes, mais la seule utilisée de nos jours est le ''cube-mapping'', où la texture de l'environnement est plaquée sur un cube, d'où son nom. Le cube en question est utilisé différemment suivant ce que l'on cherche à faire avec le ''cube-mapping''. Les deux utilisations principales sont le rendu du ciel et des décors, et les réflexions sur la surface des objets. Dans les deux cas, l'idée est de précalculer ce que l'on voit du point de vue de la caméra. On place la caméra dans la scène 3D, on place un cube centré sur la caméra, le cube est texturé avec ce que l'on voit de l'environnement depuis la caméra/l'objet de son point de vue.
[[File:Panorama cube map.png|centre|vignette|upright=2|L'illustration montre en premier lieu une ''cubemap'' avec les six faces mises en évidence, puis quel environnement 3D elle permet de simuler, le troisième illustration montrant comment la ''cubemap'' est utilisée pour simuler l'environnement.]]
Le rendu du ciel et des décors lointains dans les jeux vidéo se base sur des '''''skybox''''', à savoir un cube centré sur la caméra, sur lequel on ajoute des textures de ciel ou de décors lointains. Le cube est recouvert par une texture, qui correspond à ce que l'on voit quand on dirige le regard de la caméra vers cette face. Contrairement à ce qu'on pourrait croire, la skybox n'est pas les limites de la scène 3D, les limites du niveau d'un jeu vidéo ou quoique ce soit d'autre de lié à la physique de la scène 3D. La skybox est centrée sur la caméra, elle suit la caméra dans son mouvement. Centrer la skybox sur la caméra permet de simuler des décors très lointains, suffisamment lointain pour qu'on n'ait pas l'illusion de s'en rapprocher en se déplaçant dans la map. De plus, cela évite d'avoir à faire trop de calculs à chaque fois que l'on bouge la caméra. La texture plaquée sur le cube est une texture unique, elle-même découpée en six sous-textures, une par face du cube.
[[File:Skybox example.png|centre|vignette|upright=2|Exemple de Skybox.]]
[[File:Cube mapped reflection example.jpg|vignette|Réflexions calculées par une ''cubemap''.]]
Le ''cube-mapping'' est aussi utilisé pour des reflets. L'idée est de simuler les reflets en plaquant une texture pré-calculée sur l'objet réflecteur. La texture pré-calculée est un dessin de l'environnement qui se reflète sur l'objet, un dessin du reflet à afficher. En la plaquant la texture sur l'objet, on simule ainsi des reflets de l'environnement, mais on ne peut pas calculer d'autres reflets comme les reflets objets mobiles comme les personnages. Et il se trouve que la texture pré-calculée est une ''cubemap''. Pour les environnements ouverts, c'est la ''skybox'' qui est utilisée, ce qui permet de simuler les reflets dans les flaques d'eau ou dans des lacs/océans/autres. Pour les environnements intérieurs, c'est une cubemap spécifique qui utilisée. Par exemple, pour l'intérieur d'une maison, on a une ''cubemap'' par pièce de la maison. Les reflets se calculent en précisant quelle ''cubemap'' appliquer sur l'objet en fonction de la direction du regard.
[[File:Cube map level.png|centre|vignette|Cube map de l'intérieur d'une pièce d'un niveau de jeux vidéo.]]
Toujours est-il que les textures utilisées pour le ''cubemapping'', appelées des ''cubemaps'', sont en réalité la concaténation de six textures différentes. En mémoire vidéo, la ''cubemap'' est stockée comme six textures les unes à la suite des autres. Lors du rendu, on doit préciser quelle face du cube utiliser, ce qui fait 6 possibilités. On a le même problème qu'avec les niveaux de détail, sauf que ce sont les faces d'une ''cubemap'' qui remplacent les textures de niveaux de détails. L'accès en mémoire doit donc préciser quelle portion de la ''cubemap'' il faut accéder. Et l'accès mémoire se complexifie donc. Surtout que l'accès en question varie beaucoup suivant l'API graphique utilisée, et donc suivant la carte graphique.
Le support des ''cubemaps'' dépend de l'API 3D, et surtout de si elle date ou si elle est récente :
* Les API 3D très anciennes ne gérent pas nativement les ''cubemaps'', qui doivent être émulées en logiciel en utilisant six textures différentes. Le pixel shader décide donc quelle ''cubemap'' utiliser, avec quelques calculs sur la direction du regard.
* Les API 3D récentes gèrent nativement les ''cubemaps''. Pour les versions les plus vielles de ces API, les six faces sont numérotées et l'accès à une ''cubemap'' précise quel face utiliser en donnant son numéro. La carte graphique choisit alors automatiquement la bonne texture. Mais cela demande de laisser le calcul de la bonne face au pixel shader.
* Dans les API 3D modenres, les ''cubemap'' sont gérées comme des textures en trois dimensions, adressées avec trois coordonnées u,v,w. La carte graphique utilise ces trois coordonnées de manière à en déduire quelle est la face pertinente, mais aussi les coordonnées u,v dans la texture de la face.
==Annexe : les textures virtuelles==
Les '''textures virtuelles''' sont une optimisation des textures normales, qui visent à accélérer le rendu de terrains de grande taille. Imaginez par exemple un monde assez ouvert, comme un environnement en forêt ou en montagne, avec une grande distance de visibilité. Avec de tels terrains, le "sol" est recouvert par une texture de sol unique qui recouvre tout le terrain. Elle ne se répète pas, est de très grande taille, et peut parfois recouvrir toute la map ! Mais il n'y a pas assez de mémoire vidéo pour mémoriser la texture toute entière. La seule solution est la suivante : une partie de la texture est placée en mémoire vidéo, le reste est soit placé en mémoire RAM ou sur le disque dur.
Pour cela, le moteur de jeu utilise une optimisation ingénieuse, basée sur une observation assez basique : une bonne partie de la texture est visible, mais le reste est caché par des arbres, des habitations ou d'autres obstacles. Une optimisation possible de ne garder en mémoire vidéo que les portions visibles de la texture, pas les portions cachées. Une autre optimisation mélange textures virtuelles et ''mip-mapping''. L'idée est que pour les portions lointaines d'une texture, la texture utilisée est une ''mip-map'' de basse résolution. L'idée est alors de ne charger que la ''mip-map'' adéquate, pas les autres niveaux de détail. En clair, la texture de base n'est pas chargée en mémoire vidéo, mais la ''mip-map'' basse résolution l'est.
===Une texture à deux niveaux===
L'implémentation des textures virtuelles découpe les méga-textures en ''tiles'', en morceaux rectangulaires de taille modeste. En clair, le terrain est découpé en morceau rectangulaires/carrés. Seules les tiles nécessaires sont chargées en mémoire vidéo, pas les autres. Par exemple, les ''tiles'' non-visibles ne sont pas placées en mémoire vidéo, seules les ''tiles'' visibles le sont. De même, il y a une ''tile'' par niveau de mip-map : seul la tile correspondant le niveau adéquat est en mémoire vidéo, les autres niveaux de détail ne sont pas chargés. On peut faire une analogie avec la mémoire virtuelle, où les données sont découpées en pages, qui sont chargées en mémoire RAM à la demande, suivant les besoins, les données pouvant être swappées sur le disque dur si elles sont peu utilisées. Sauf qu'ici, il s'agit de textures qui sont découpées en pages chargées à la demande en mémoire vidéo, depuis la RAM système.
Une texture virtuelle est en réalité un système à deux niveaux : une liste de ''tiles'' et les ''tiles'' elles-mêmes. La liste de ''tiles'' est appelée un '''atlas de texture''', c'est un peu l'équivalent de la ''tilemap'' pour le rendu 2D. Rendre une texture demande de calculer quelle ''tile'' contient le texel à afficher, consulter la ''tile'' en question, puis récupérer le texel adéquat dans cette ''tile''. La ''tile'' est donc une texture, mais la texture à charger est choisie parmi un ensemble, qui est ici l'atlas de texture.
===L'implémentation : logicielle versus matérielle===
Les textures virtuelles ont été utilisées pour la première fois par les jeux Rage 1 et 2 d'IdSoftware, et quelques jeux ultérieurs comme DOOM 2016. IdSoftware les appelait des '''''mega-textures'''''. L'optimisation permettait des gains en performance assez impressionnants. Le jeu Rage 1 utilisait une texture carrée unique de 128k pixels de côté pour rendre le terrain. En théorie, une telle texture devrait prendre 64 giga-octets, mais le jeu tournait correctement avec 512 méga-octets de RAM, poussivement avec seulement 256 méga-octets de RAM.
De nos jours, les textures virtuelles sont supportées par beaucoup de jeux vidéos, les moteurs les plus courants gèrent de telles textures de manière logicielles. Mais quelques GPU récents supportent les textures virtuelles. Sur les GPU récents, l'atlas de texture est géré nativement par le matériel. Le GPU choisit quelle ''tile'', quelle texture choisir pour rendre le texel adéquat. Pour cela, le GPU calcule quelle ''tile'' charger, consulte l'atlas de texture, et lit la texture de ''tile'' adéquate.
Mais l'implémentation sur les GPU récents a de nombreuses limitations. La limitation la plus importante est que la taille des textures virtuelles ne peut pas dépasser la taille d'une texture normale, soit 32768 pixels de côté pour une texture carrée environ sur les GPU de 2020. De plus, le chargement d'une ''tile'' est très lent. En clair, dès qu'on veut changer de niveau de mip-map pour une tile, ou dès qu'une tile devient visible, le chargement de la tile peut facilement prendre plusieurs centaines de millisecondes. Le filtrage de texture est très complexe avec des textures virtuelles, ce qui fait que le filtrage de texture virtuelle est souvent soumis à des limitations que les textures normales n'ont pas, notamment pour le filtrage anisotropique.
{{NavChapitre | book=Les cartes graphiques
| prev=Le rasterizeur
| prevText=Le rasterizeur
| next=Les Render Output Target
| nextText=Les Render Output Target
}}{{autocat}}
pkoq49whxbo5ui28p5d7zn7qfuhi27x
763423
763422
2026-04-10T19:29:57Z
Mewtow
31375
/* Annexe : le cube-mapping */
763423
wikitext
text/x-wiki
[[File:Texture mapping.png|vignette|''Texture mapping'']]
Les '''textures''' sont des images que l'on va plaquer sur la surface d'un objet, du papier peint en quelque sorte. Les cartes graphiques supportent divers formats de textures, qui indiquent comment les pixels de l'image sont stockés en mémoire : RGB, RGBA, niveaux de gris, etc. Une texture est donc composée de "pixels", comme toute image numérique. Pour bien faire la différence entre les pixels d'une texture, et les pixels de l'écran, les pixels d'une texture sont couramment appelés des ''texels''.
==Le placage de textures inverse==
Pour rappel, plaquer une texture sur un objet consiste à attribuer un texel à chaque sommet, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture.
Dans les faits, on n'utilise pas de coordonnées entières de ce type. Les coordonnées de texture sont deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. Le nom donnée à cette technique de description des coordonnées de texture s'appelle l''''''UV Mapping'''''.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Les API 3D modernes gèrent des textures en trois dimensions, ce qui ajoute une troisième coordonnée de texture notée w. Dans ce qui va suivre, nous allons passer les textures en trois dimensions sous silence. Elles ne sont pas très utilisées, la quasi-totalité des jeux vidéo et applications 3D utilisant des textures en deux dimensions. Par contre, le matériel doit gérer les textures 3D, ce qui le rend plus complexe que prévu. Il faut ajouter quelques circuits pour, de quoi gérer la troisième coordonnée de texture, etc.
Lors de la rastérisation, chaque fragment se voit attribuer un sommet, et donc la coordonnée de texture qui va avec. Si un pixel est situé pile sur un sommet, la coordonnée de texture de ce sommet est attribuée au pixel. Si ce n'est pas le cas, la coordonnée de texture finale est interpolée à partir des coordonnées des trois sommets du triangle rastérisé. L'interpolation en question a lieu dans l'étape de rastérisation, comme nous l'avons vu dans le chapitre précédent. Le fait qu'il y ait une interpolation fait que les coordonnées du pixel gagent à être des nombres flottants. On pourrait faire une interpolation avec des coordonnées de texture entières, mais les arrondis et autres imprécisions de calcul donneraient un résultat graphiquement pas terrible, et empêcheraient d'utiliser les techniques de filtrage de texture que nous verrons dans ce chapitre.
À partir de ces coordonnées de texture, la carte graphique calcule l'adresse du texel qui correspond, et se charge de le lire. Et toute la magie a lieu dans ce calcul d'adresse, qui part de coordonnées de texture flottante, pour arriver à une adresse mémoire. Le calcul de l'adresse du texel se fait en plusieurs étapes, que nous allons voir ci-dessous. La première étape convertit les coordonnées flottantes en coordonnées entières, qui disent à quel ligne et colonne se trouve le texel voulu dans la texture. L'étape suivante transforme ces coordonnées x,y entières en adresse mémoire.
===La normalisation des coordonnées===
J'ai dit plus haut que les coordonnées de texture sont des coordonnées flottantes, comprises entre 0 et 1. Mais il faut savoir que les pixels shaders peuvent modifier celles-ci pour mettre en œuvre certains effets graphiques. Et le résultat peut alors se retrouver en-dehors de l'intervalle 0,1. C'est quelque chose de voulu et qui est traité par la carte graphique automatiquement, sans que ce soit une erreur. Au contraire, la manière dont la carte graphique traite cette situation permet d'implémenter des effets graphiques comme des textures en damier ou en miroir.
[[File:Clamp tile.jpg|vignette|Clamp tile]]
Il existe globalement trois méthodes très simples pour gérer cette situation, qui sont appelés des '''modes d'adressage de texture'''.
* La première méthode est de faire en sorte que le résultat sature. Si une coordonnée est inférieur à 0, alors on la remplace par un zéro. Si elle est supérieure à 1, on la ramène à 1. Avec cette méthode, tout se passe comme si les bords de la texture étaient étendus et remplissaient tout l'espace autour de la texture. Le tout est illustré ci-dessous. Ce mode d'accès aux textures est appelé le '''''clamp'''''.
* Une autre solution retire la partie entière de la coordonnée, elle coupe tout ce qui dépasse 1. Pour le dire autrement, elle calcule le résultat modulo 1 de la coordonnée. Le résultat est que tout se passe comme si la texture était répétée à l'infini et qu'elle pavait le plan.
* Une autre méthode remplit les coordonnées qui sortent de l’intervalle 0,1 avec une couleur préétablie, configurée par le programmeur.
===La conversion des coordonnées de textures flottantes en adresse mémoire===
Une fois la normalisation effectuée, les coordonnées de texture sont utilisées pour lire le texel voulu. Pour cela, les coordonnées de texte sont transformées en adresse mémoire, adresse qui pointe sur le texel ayant ces cordonnées. Pour cela, la première étape est de transformer les coordonnées flottantes u,v en coordonnées entières x,y qui pointent sur un texel. Pour cela, il suffit de multiplier les coordonnées flottantes u,v par la résolution de la texture accédée. Pour un écran de résolution <math>\text{height,width}</math>, le calcul est le suivant :
: <math>x = u \times \text{width}</math>
: <math>y = v \times \text{height}</math>
Le résultat est un nombre avec une partie entière et une partie fractionnaire. La partie entière des deux coordonnées donne la position x,y voulue, et la partie fractionnaire est conservée pour le filtrage de textures, mais passons cela sous silence pour le moment.
La seconde étape prend les coordonnées entières x,y et calcule l'adresse mémoire du texel. L'adresse dépend de la position de la texture en mémoire, précisément de son début, son premier texel, mais aussi de la position du texel par rapport au début de la texture. Et calculer cette position intra-texture dépend de la manière dont les texels sont stockés en mémoire.
====Les textures naïves====
Les programmeurs qui lisent ce cours s'attendent certainement à ce que la texture soit stockée en mémoire ligne par ligne, ou colonne par colonne. Cela veut dire que le premier pixel en partant d'en haut à gauche est stocké en premier, puis celui immédiatement à sa droite, puis celui encore à droite, et ainsi de suite. Une fois qu'on arrive à la fin d'une ligne, on passe à la ligne suivante, en-dessous. Cette organisation ligne par ligne s'appele l'organisation '''''row major order'''''. On peut faire pareil, mais colonne par colonne, ce qui donne le '''''column major order'''''.
[[File:Speicheranordnung Feld.svg|centre|vignette|upright=2|Row et column major order.]]
Maintenant, supposons que la texture commence à l'adresse <math>A_\text{texture}</math>, qui est l'adresse du premier texel. La texture a une résolution de <math>\text{width}</math> texels de large et <math>\text{height}</math> texels de haut. Par définition, les coordonnées X et Y des texels commencent à 0, ce qui fait que le pixel en haut à gauche a les coordonnées 0,0.
L'adresse du pixel se calcule comme suit :
: <math>A_\text{pixel} = A_\text{texture} + (\text{taille d'une ligne en octets} \times Y) + (\text{taille d'un texel en octets} \times X)</math>
La taille d'un pixel en mémoire est notée T. La taille d'une ligne en mémoire est de <math>width \times T</math>, par définition, vu qu'elle fait <math>width</math> texels. On a donc :
: <math>A_\text{pixel} = A_\text{texture} + (width \times T \times Y) + (T \times X)</math>
La formule se réécrit comme suit :
: <math>A_\text{pixel} = A_\text{texture} + T \times (width \times Y + X)</math>
Le calcul d'adresse est donc assez simple. Malheureusement, les textures ne sont pas stockées de cette manière en mémoire vidéo. En effet, elle se marie mal avec les opérations de filtrage de texture que nous allons voir dans ce qui suit. Le filtrage d'un texel dépend de ses voisins du dessus et du dessous. Le fait que la texture n'est pas forcément parcourue ligne par ligne fait que stocker une texture ligne par ligne n'est pas l'idéal.
De même, les textures sont déformées par la perspective. L'affichage de la texture ne se fait alors pas ligne par ligne, mais en parcourant la texture en diagonale, l'angle de la diagonale correspondant approximativement à l'angle que fait la verticale de la texture avec le regard. Vu qu'on ne connait pas à l'avance l'angle que fera la diagonale de parcours, on doit ruser.
====Les textures tilées====
Une première solution à ce problème est celle des '''textures tilées'''. Avec ces textures, l'image de la texture est découpée en ''tiles'', des rectangles ou en carrés de taille fixe, généralement des carrés de 4 pixels de côté. Les tiles ont une largeur et une longueur égales, afin de simplifier les calculs : on divise X et Y par le même nombre. De plus, leur largeur et leur longueur sont une puissance de deux, afin de simplifier les calculs d'adresse. Les ''tiles'' sont alors mémorisée les unes après les autres dans le fichier de la texture.
[[File:Texture tilée.png|centre|vignette|upright=2|Texture tilée]]
La formule de calcul d'adresse vue plus haut doit être adaptée pour tenir compte des tiles. Pour cela, il faut remplacer la taille d'un texel par la taille d'une tile, et que la largeur de la texture soit exprimée en nombre de tiles. De plus, on doit adapter les coordonnées des texels pour donner des coordonnées de tile. Généralement, les tiles sont des carrés de N pixels de côté, ce qui fait qu'on peut regrouper les lignes et les colonnes par paquets de N. Il suffit donc de diviser Y et X pour obtenir les coordonnées de la tile, de même que la larguer. La formule pour calculer la position de la énième tile est alors la suivante :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + \text{Taille mémoire d'une tile} \times \left( {\text{Width} \over N} \times {Y \over N} + {X \over N} \right)</math>
On peut réécrire le tout comme suit :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + K \times \left( {Y \over N} + X \right)</math>, avec K une constante connue à la compilation des shaders.
Vu que les tiles sont carrées avec une largeur qui est une puissance de deux, la multiplication par la taille d'une tile en mémoire se simplifie : on passe d'une multiplication entière à des décalages de bits. Même chose pour le calcul de l'adresse de la tile à partir des coordonnées x,y : ils impliquent des divisions par une puissance de deux, qui deviennent de simples décalages.
La position d'un pixel dans une tile dépend du format de la texture, mais peut se calculer avec quelques calculs arithmétiques simples. Dans les cas les plus simples, les pixels sont mémorisés ligne par ligne, ou colonne par colonne. Mais ce n'est pas systématiquement le cas. Toujours est-il que les calculs pour déterminer l'adresse sont simples, et ne demandent que quelques additions ou multiplications. Mais avec les formats de texture utilisés actuellement, les tiles sont chargées en entier dans le cache de texture, sans compter que diverses techniques de compression viennent mettre le bazar, comme on le verra dans la suite de cours.
Un avantage de l'organisation en tiles est qu'elle se marie bien avec le parcours des textures. On peut parcourir une texture dans tous les sens, horizontal, vertical, ou diagonal, on sait que les prochains pixels ont de fortes chances d'être dans la même tile. Si on rentre dans une tile par la gauche en haut, on a encore quelques pixels à parcourir dans la tile, par exemple. De même, le filtrage de textures est facilité. On verra dans ce qui va suivre que le filtrage de texture a besoin de lire des blocs de 4 texels, des carrés de 2 pixels de côté. Avec l'organisation en tile, on est certain que les 4 texels seront dans la même tile, sauf s'ils ont le malheur d'être tout au bord d'une tile. Ce dernier cas est assez rare, et il l'est d'autant plus que les tiles sont grandes. Enfin, un dernier avantage est que les tiles sont généralement assez petites pour tenir tout entier dans une ligne de cache. Le cache de texture est donc utilisé à merveille, ce qui rend les accès aux textures plus rapides.
====Les textures basées sur des ''z-order curves''====
Les formats de textures théoriquement optimaux utilisent une '''''Z-order curve''''', illustrée ci-dessous. L'idée est de découper la texture en quatre rectangles identiques, et de stocker ceux-ci les uns à la suite des autres. L'intérieur de ces rectangles est lui aussi découpé en quatre rectangles, et ainsi de suite. Au final, l'ordre des pixels en mémoire est celui illustré ci-dessous.
[[File:Z-CURVE.svg|centre|vignette|upright=2|Construction d'une ''Z-order curve''.]]
Les texels sont stockés les uns à la suite des autres dans la mémoire, en suivant l'ordre donnée par la ''Z-order curve''. Le calcul d'adresse calcule la position du texel en mémoire, par rapport au début de la texture, et ajoute l'adresse du début de la texture. Mais tout le défi est de calculer la position d'un texel en mémoire, à partir des coordonnées x,y. Le calcul peut sembler très compliqué, mais il n'en est rien. Le calcul demande juste de regarder les bits des deux coordonnées et de les combiner d'une manière particulièrement simple. Il suffit de placer le bit de poids fort de la coordonnée x, suivi de celui de la coordonnée y, et de faire ainsi de suite en passant aux bits suivants.
[[File:Zcurve45bits.png|centre|vignette|upright=1.5|Calcul de la position d'un élément dans une ''Z-order curve'' à partir des coordonnées x et y.]]
L'avantage d'une telle organisation est que la textures est découpées en ''tiles'' rectangulaires d'une certaine taille, elles-mêmes découpées en ''tiles'' plus petites, etc. Et il se trouve que cette organisation est parfaite pour le cache de texture. L'idéal pour le cache de texture est de charger une ''tile'' complète dans le cache de textures. Quand on accède à un texel, on s'assure que la ''tile'' complète soit chargée. Mais cela demande de connaitre à l'avance la taille d'une ''tile''. Les formats de texture fournissent généralement une ''tile'' carré de 4 pixels de côté, mais cela donnerait un cache trop petit pour être vraiment utile. Avec cette méthode, on s'assure qu'il y ait une ''tile'' avec la taille optimale. Les ''tiles'' étant découpées en ''tiles'' plus petites, elles-mêmes découpées, et ainsi de suite, on s'assure que la texture est découpées en ''tiles'' de taille variées. Il y aura au moins une ''tile'' qui rentrera tout pile dans le cache.
===Les techniques de rendu à textures multiples===
Nous venons de voir comment une texture est plaquée sur un objet 3D, ou une surface comme un sol. Pour résumer, le calcul de l'adresse d'un texel prend la position du texel par rapport au début de la texture, et ajoute l'adresse du début de la texture. L'adresse mémoire de la texture est connue au moment où le pilote de la carte graphique place la texture dans la mémoire vidéo, et cette information est transmise au matériel par l'intermédiaire du processeur de commande, puis passée aux processeurs de shaders et à l'unité de texture. Le tout est couplé à d'autres informations, la plus importante étant la ''taille de la texture en octets'', pour éviter de déborder lors des accès à la texture.
Néanmoins, il s'agit là du cas le plus simple. Certaines techniques de rendu demandent de choisir la texture à plaquer parmi un ensemble de plusieurs textures. Les techniques en question sont assez variées et n'ont pas grand chose en commun. Les plus connues sont le ''mip-mapping'', le ''cube-mapping'' et les textures virtuelles. Le ''mip-mapping'' sert à filtrer les textures, chose qu'on expliquera plus tard, le ''cube-mapping'' sert à simuler des réflexions sur un objet en plaquant une texture de l'environnement dessus, les textures virtuelles sont une optimisation pour les textures des terrains de grande taille. Mais malgré leurs différences, elles demandent de choisir quelle texture plaquer entre plusieurs textures de base. En clair, l'adresse de base de la texture varie selon la situation. Voyons-les dans le détail.
==L'implémentation matérielle du placage de textures==
Pour résumer, la lecture d'un texel demande d'effectuer plusieurs étapes. Dans le cas le plus simple, sans ''mip-mapping'' ou ''cubemapping'', on doit effectuer les étapes suivantes :
* Il faut d'abord normaliser les coordonnées de texture pour qu'elles tombent dans l'intervalle [0,1] en fonction du mode d'adressage désiré.
* Ensuite, les coordonnées u,v doivent être converties en coordonnées entières, ce qui demande une multiplication flottante.
* Enfin, l'adresse finale est calculée à partir des coordonnées entières et en ajoutant l'adresse de base de la texture (et éventuellement avec d'autres calculs arithmétiques suivant le format de la texture).
Tout cela pourrait être fait par le pixel shaders, mais cela implique beaucoup de calculs répétitifs et d'opérations arithmétiques assez lourdes, avec des multiplications flottantes, des additions et des multiplications entières, etc. Faire faire tous ces calculs par les shaders serait couteux en performance, sans compter que les shaders deviendraient plus gros et que cela aurait des conséquences sur le cache d'instruction. De plus, certaines de ces étapes peuvent se faire en parallèle, comme les deux premières, ce qui colle mal avec l'aspect sériel des shaders.
Aussi, les processeurs de shaders incorporent une unité de calcul d'adresse spéciale pour faire ces calculs directement en matériel. L'unité de calcul en question est dans l'unité de texture. Cette dernière contient donc au minimum deux circuits : un circuit de calcul d'adresse, et un circuit d'accès à la mémoire. Toute la difficulté tient dans le calcul d'adresse, plus que dans le circuit de lecture. Le calcul d'adresse est conceptuellement réalisé en deux étapes. La première étape qui transforme les coordonnées u,v en coordonnées x,y qui donne le numéro de la ligne et de la colonne du texel dans la texture. La seconde étape prend ces deux coordonnées x,y, l'adresse de la texture, et détermine l'adresse de la tile à lire.
[[File:Unité de texture simple.png|centre|vignette|upright=2|Unité de texture simple]]
===La gestion des accès mémoire===
Enfin, l'unité de texture doit tenir compte du fait que la mémoire vidéo met du temps à lire une texture. En théorie, l'unité de texture ne devrait pas accepter de nouvelle demande de lecture tant que celle en cours n'est pas terminée. Mais faire ainsi demanderait de bloquer tout le pipeline, de l'''input assembler'' au unités de''shaders'', ce qui est tout sauf pratique et nuirait grandement aux performances.
Une solution alternative consiste à mettre en attente les demandes de lectures de texture pendant que la mémoire est occupée. La manière la plus simple d'implémenter des accès mémoire multiples est de les mettre en attente dans une petite mémoire FIFO. Cela implique que les accès mémoire s’exécutent dans l'ordre demandé par le ''shader'' et/ou l'unité de rastérisation, il n'y a pas de réorganisation des accès mémoire ou d’exécution dans le désordre des accès mémoire.
[[File:Texture prefetching.png|centre|vignette|upright=1.5|Accès mémoire simultanés.]]
Évidemment, quand la mémoire FIFO est pleine, le pipeline est alors totalement bloqué. Le rasteriser est prévenu que l'unité de texture ne peut pas accepter de nouvelle lecture de texture. En pratique, la FIFO est généralement d'une taille respectable et permet de mettre en attente beaucoup de demandes de lecture de texture. Il faut de plus noter qu'il y a une FIFO par processeur de ''shader'' sur les cartes graphiques modernes. Quand elle est pleine, le processeur cesse d'exécuter de nouveaux accès mémoire, mais peut continuer à exécuter des ''shaders'' dans les autres unités de calcul, pas besoin de bloquer complétement le pipeline.
===L'intégration du cache de textures===
Il faut noter que les unités de texture incorporent aussi un cache de texture, voire plusieurs. L'intégration des caches de texture avec la mémoire FIFO précédente est quelque peu compliqué, car il faut garantir que les lectures de texture se fassent dans le bon ordre. On ne peut pas exécuter une lecture dans le cache alors que des lectures précédentes sont en attente de lecture en mémoire vidéo. Et cela pose un gros problème : une lecture dans le cache de texture prend quelques dizaines de cycles d'horloge, alors qu'une lecture en mémoire vidéo en prend facilement 400 à 800 cycles, parfois plus. Et cela fait que l'ordre des accès mémoire peut s'inverser.
Prenons par exemple un accès au cache précédé et suivi par deux accès en mémoire vidéo. Le premier démarre au cycle 1, et se termine au cycle numéro 400. L'accès au cache commence au cycle 2 et se termine 20 cycles après, au cycle numéro 22. En clair, la lecture dans le cache s'est terminée avant l'accès mémoire qui le précède. Les textures ne sont donc plus lues dans l'ordre. Et il faut trouver une solution pour éviter cela.
La solution est de retarder les lectures dans le cache tant que tous les accès précédents ne sont pas terminés. Mais pour retarder les lectures en question, il faut d'abord savoir si la lecture atterrit dans le cache ou non, ce qui demande d'accéder au cache. On fait face à un dilemme : on veut retarder les accès au cache, mais les différencier des lectures déclenchant des accès mémoire demande d'accéder au cache en premier lieu. La solution est décrite dans l'article "Prefetching in a Texture Cache Architecture" par Igehy et ses collègues. Elle se base sur deux idées combinées ensemble.
La première idée est de séparer l'accès au cache en deux : une étape qui vérifie si les texels à lire sont dans le cache, et une étape qui accède aux données dans le cache lui-même. Un cache de texture est donc composé de deux circuits principaux. Le premier vérifie la présence des texels dans le cache. Il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Ensuite, en plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
La seconde idée est de retarder l'accès au cache entre les deux phases. La première étape d'un accès mémoire vérifie si la donnée est dans le cache ou non. Puis, on retarde la lecture des données, pour attendre que toutes les lectures précédentes soient terminées. Et enfin, troisième étape : la lecture des texels dans la mémoire cache proprement dite. Les accès mémoire passant par la mémoire vidéo se font de la même manière, à une différence près : la lecture dans le cache est remplacée par la lecture en mémoire vidéo. Tout démarre avec une demande à l'unité de tags, qui vérifie si le texel est dans le cache ou non. Puis on retarde l'accès tant que la mémoire vidéo est occupée, puis on effectue la lecture en mémoire vidéo.
Si ce n'est pas le cas, l'accès mémoire est envoyé à la mémoire vidéo comme précédemment, à savoir qu'il est mis en attente dans une mémoire FIFO, puis envoyé à la mémoire vidéo dès que celle-ci est libre. Mais en sortie de la mémoire, la donnée lue est envoyée dans le cache de texture, par dans l'unité de filtrage. Pour savoir où placer la donnée lue, l'unité de tag a réservé une ligne de cache précise, une adresse bien précise. L'adresse en question est disponible en lisant une autre mémoire FIFO, qui a mis en attente l'adresse en question, en attendant que l'accès mémoire se termine. La donnée est alors écrite dans le cache, puis lue par l'unité de filtrage de textures.
Pour une lecture dans le cache, le déroulement est similaire, mais sans le passage par la mémoire. La lecture fait une demande à l'unité de tag, et celle-ci répond que la donnée est bien dans le cache. Elle place alors l'adresse à lire dans la file d'attente. Une fois que les accès mémoire précédents sont terminés, l'adresse sort de la file d'attente et est envoyée à la mémoire de données. La lecture s'effectue, les texels sont envoyés à l'unité de filtrage de textures. La seule différence avec un ''phased cache'' normal est l'insertion de l'adresse à lire dans une FIFO qui vise à mettre en attente
[[File:Unité de texture avec un cache de texture.png|centre|vignette|upright=2.0|Unité de texture avec un cache de texture]]
Pour résumer, l'implémentation précédente garantit une exécution des lectures dans leur ordre d'arrivée. Et pour cela, elle retarde les lectures dans le cache tant que les lectures en mémoire précédentes ne sont pas terminées. L'accès au cache est plus rapide que l'accès en mémoire vidéo, mais le retard ajouté pour garantir l'ordre des lectures fait que le temps d'accès est très long.
==Le mip-mapping==
Le '''mip-mapping''' a pour but de légèrement améliorer les graphismes des objets lointains, tout en rendant les calculs de texture plus rapides. Formellement, le ''mip-mapping'' est une technique de filtrage de texture, mais nous l'abordons maintenant car elle est surtout liée au calcul d'adresse. Les unités de texture ont des circuits de filtrage de texture séparés des circuits de ''mip-mapping'' et de calcul d'adresse, d'où le fait que nous en parlons séparément.
Le problème résolu par le ''mip-mapping'' est le rendu des textures lointaines. Si une texture est plaquée sur un objet lointain, une bonne partie des détails est invisible pour l'utilisateur. Un pixel de l'écran est associé à plusieurs texels. Idéalement, la carte graphiques devrait lire tous ces texels et en faire une sorte de moyenne pondérée, pour calculer la couleur finale du pixel. Mais dans les faits, ce serait très gourmand et compliqué à implémenter en hardware. Une solution serait de ne garder que quelque texels, mais cela a tendance à créer des artefacts visuels (les textures affichées ont tendance à pixeliser). Le ''mip-mapping'' permet de réduire ces deux problèmes en même temps en précalculant cette moyenne pondérée pour des distances prédéfinies.
L'idée est d'utiliser plusieurs exemplaires d'une même texture à des résolutions différentes, chaque exemplaire étant adapté à une certaine distance. Par exemple, une texture sera stocké avec un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre de 128 * 128 et ainsi de suite jusqu’à un dernier exemplaire de 32 * 32 pixel. Chaque exemplaire correspond à un '''niveau de détail''', aussi appelé ''Level Of Detail'' (abrévié en LOD). La résolution utilisée diminue d'autant plus que l'objet est situé loin de la caméra. Les objets proches seront rendus avec la texture 512*512, ceux plus lointains seront rendus avec la texture de résolution 256*256, les textures 128*128 seront utilisées encore plus loin, et ainsi de suite jusqu'aux objets les plus lointains qui sont rendus avec la texture la plus petite de 32*32.
[[File:MipMap Example STS101.jpg|centre|vignette|upright=2|Exemples de mip-maps.]]
Le ''mip-mapping'' améliore grandement la qualité d'image. L'image d'exemple ci-dessous le montre assez bien.
[[File:Mipmapping example.png|centre|vignette|upright=2|Exemple de mipmapping.]]
Pour faciliter les calculs d'adresse, les LOD d'une même texture sont stockées les uns après les autres en mémoire (dans un tableau, comme diraient les programmeurs). Ainsi, pas besoin de se souvenir de la position en mémoire de chaque LOD : l'adresse de la texture de base, et quelques astuces arithmétiques suffisent. Prenons le cas où la texture de base a une taille L. le premier exemplaire est à l'adresse 0, le second niveau de détail est à l'adresse L, le troisième à l'adresse L + L/4, le suivant à l'adresse L + L/4 + L/16, et ainsi de suite. Le calcul d'adresse demande juste connaître le niveau de détails souhaité et l'adresse de base de la texture. Le niveau de détail voulu est calculé par les pixel shaders, en fonction de la coordonnée de profondeur du pixel à traiter.
Évidemment, cette technique consomme de la mémoire vidéo, vu que chaque texture est dupliquée en plusieurs exemplaires, en plusieurs LOD. Dans le détail, la technique du mip-mapping prend au maximum 33% de mémoire en plus (sans compression). Cela vient du fait qu'en prenant une texture dexu fois plus petite, elle prend 4 fois moins de mémoire : 2 fois moins de pixels en largeur, et 2 fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des LODs, texture de base comprise, prendra X + (X/4) + (X/16) + (X/256) + … Un petit calcul de limite donne 4/3 * X, soit 33% de plus.
===L'implémentation du mip-mapping dans l'unité de texture===
Le ''mip-mapping'' est lui aussi pris en charge par l'unité de calcul d'adresse, car cette technique change l'adresse de base de la texture. La gestion du ''mip-mapping'' est cependant assez complexe. Il est possible de laisser le pixel shader calculer quel niveau de détail utiliser, en fonction de la coordonnée de profondeur z du pixel à afficher. La carte graphique détermine alors automatiquement quelle texture lire, quel niveau de détail, automatiquement. Elle détermine aussi la bonne résolution pour la texture, qui est égal à la résolution de la texture de base, divisée par le niveau de détail. Pour résumer, le niveau de détail est envoyé aux unités de texture, qui s'occupent de calculer l'adresse de base et la résolution adéquates. Quelques calculs arithmétiques simples, donc, qui s'implémentent facilement avec quelques circuits.
Mais une autre méthode laisse la carte graphique déterminer le niveau de détail par elle-même. Dans ce cas, cela demande, outre les deux coordonnées de texture, de calculer la dérivée de ces deux coordonnées dans le sens horizontal et vertical, ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Les quatre dérivées sont les suivantes :
: <math>\frac{du}{dx}</math>, <math>\frac{dv}{dx}</math>, <math>\frac{du}{dy}</math>, <math>\frac{dv}{dy}</math>
Un bon moyen pour obtenir les dérivées demande de regrouper les pixels par groupes de 4 et de faire la différence entre leurs coordonnées de texture respectives. On peut calculer les deux dérivées horizontales en comparant les deux pixels sur la même ligne, et les deux dérivées verticales en comparant les deux pixels sur la même colonne. Mais cela demande de rastériser les pixels par groupes de 4, par ''quads''. Et c'est ce qui est fait sur les cartes graphiques actuelles, qui rastérisent des groupes de 4 pixels à la fois.
[[File:Texture sampler unit with mipmapping.png|centre|vignette|upright=2.0|Unité de texture avec mipmapping.]]
Malheureusement, le calcul exact utilisé pour le choix de la mip-map dépend du GPU considéré et peu de chose est connu quant à ces algorithmes. Il est possible d'inférer le comportement à partir d'observations, mais guère plus. Pour ceux qui veulent en savoir plus, je conseille la lecture de cet article de blog :
* [https://pema.dev/2025/05/09/mipmaps-too-much-detail/ Mipmap selection in too much detail]
==Le filtrage de textures==
Plaquer des textures sans autre forme de procès ne suffit pas à garantir des graphismes d'une qualité époustouflante. La raison est que les sommets et les texels ne tombent pas tout pile sur un pixel de l'écran : le sommet associé au texel peut être un petit peu trop en haut, ou trop à gauche, etc. Une explication plus concrète fait intervenir les coordonnées de texture. Souvenez-vous que lorsque l'on traduit une coordonnée de texture u,v en coordonnées x,y, on obtient un résultat qui ne tombe pas forcément juste. Souvent, le résultat a une partie fractionnaire. Si celle-ci est non-nulle, cela signifie que le texel/sommet n'est pas situé exactement sur le pixel voulu et que celui-ci est situé à une certaine distance. Concrètement, le pixel tombe entre quatre texels, comme indiqué ci-dessous.
[[File:Filtrage texture.png|centre|vignette|upright=2.0|Position du pixel par rapport aux texels.]]
Pour résoudre ce problème, on doit utiliser différentes techniques d'interpolation, aussi appelées techniques de '''filtrage de texture''', qui visent à calculer la couleur du pixel final en fonction des texels qui l'entourent. Il existe de nombreux types de filtrage de textures, qu'il s'agisse du filtrage linéaire, bilinéaire, trilinéaire, anisotropique et bien d'autres.
Tous ont besoin d'avoir certaines informations qui sont généralement fournies par les circuits de calcul d'adresse. La première est clairement la partie fractionnaire des coordonnées x,y. La seconde est la dérivée de ces deux coordonnées dans le sens horizontal et vertical., ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Toujours est-il que le filtrage de texture est une opération assez lourde, qui demande beaucoup de calculs arithmétiques. On pourrait en théorie le faire dans les pixels shaders, mais le cout en performance serait absolument insoutenable. Aussi, les cartes graphiques intègrent toutes un circuit dédié au filtrage de texture, le ''texture sampler''. Même les plus anciennes cartes graphiques incorporent une unité de filtrage de texture, ce qui nous montre à quel point cette opération est importante.
[[File:Texture unit.png|centre|vignette|upright=2.0|Unité de texture.]]
On peut configurer la carte graphique de manière à ce qu'elle fasse soit du filtrage bilinéaire, soit du filtrage trilinéaire, on peut configurer le niveau de filtrage anisotropique, etc. Cela peut se faire dans les options de la carte graphique, mais cela peut aussi être géré par l'application. La majorité des jeux vidéos permettent de régler cela dans les options. Ces réglages ne concernent pas la texture elle-même, mais plutôt la manière dont l'unité de texture doit fonctionner. Ces réglages sur l''''état de l'unité de texture''' sont mémorisés quelque part, soit dans l'unité de texture elle-même, soit fournies avec la ressource de texture elle-même, tout dépend de la carte graphique. Certaines cartes graphiques mémorisent ces réglages dans les unités de texture ou dans le processeur de commande, et tout changement demande alors de réinitialiser l'état des unités de texture, ce qui prend un peu de temps. D'autres placent ces réglages dans les ressources de texture elles-mêmes, ce qui rend les modifications de configuration plus rapides, mais demande plus de circuits. D'autres cartes graphiques mélangent les deux options, certains réglages étant globaux, d'autres transmis avec la texture. Bref, difficile de faire des généralités, tout dépend du matériel et le pilote de la carte graphique cache tout cela sous le tapis.
Maintenant que cela est dit, voyons quelles sont les différentes méthodes de filtrage de texture et comment la carte graphique fait pour les calculer.
===Le filtrage au plus proche===
La méthode de filtrage la plus simple consiste à colorier avec le texel le plus proche. Cela revient tout simplement à ne pas tenir compte de la partie fractionnaire des coordonnées x,y, ce qui est très simple à implémenter en matériel. C'est ce que l'on appelle le '''filtrage au plus proche''', aussi appelé ''nearest filtering''.
Autant être franc, le résultat est assez pixelisé et peu agréable à l’œil. Par contre, le résultat est très rapide à calculer, vu qu'il ne demande aucun calcul à proprement parler. Elle ne fait pas appel à la parti fractionnaire des coordonnées entières de texture, ni aux dérivées de ces coordonnées. On peut combiner cette technique avec le mip-mapping, ce qui donne un résultat bien meilleur, bien que loin d'être satisfaisant. Au passage, toutes les techniques de filtrage de texture peuvent se combiner avec du mip-mapping, certaines ne pouvant pas faire sans.
[[File:Interpolation-nearest.svg|centre|vignette|Filtrage de texture au plus proche.]]
===Le filtrage linéaire===
Le filtrage le plus simple est le '''filtrage linéaire'''. Il effectue une interpolation linéaire entre deux mip-maps, deux niveaux de détails. Pour comprendre l'idée, nous allons prendre une situation très simple, avec une texture carrée de 512 texels de côté. Le mip-mapping crée plusieurs textures : une de 256 texels de côté, une de 128 texels, une de 64, etc. Maintenant, la texture est sur un objet à une certaine distance de l'écran, vu de face. Le résultat est qu'elle correspond à l'écran à un carré de 300 pixels de côté (pas d'erreur : pixels, pas texels). Dans ce cas, la texture se trouve entre deux mip-maps : celle de 512 pixels de côté, celle de 256. Laquelle choisir ? Le filtrage au plus proche prend la texture de 512 pixels de côté. Le filtrage linéaire lui, fait autrement.
Vu que la texture est entre deux mip-maps, l'idée est de prendre le texel au plus proche dans chaque texture et de faire une sorte de moyenne appelée l'interpolation linéaire. L'interpolation par du principe que la couleur varie entre les deux texels en suivant une fonction affine, illustrée ci-dessous. Ce ne serait évidemment pas le cas dans le monde réel, mais on supposer cela donne une bonne approximation de ce à quoi ressemblerait une texture à plus haute résolution. On peut alors calculer la couleur du pixel par une simple moyenne pondérée par la distance. Le résultat est que les transitions entre deux niveaux de détails sont plus lisses, moins abruptes.
[[File:Lin interp -é.png|centre|vignette|upright=2.0|Interpolation linéaire.]]
===Le filtrage bilinéaire===
Le filtrage bilinéaire effectue une sorte de moyenne pondérée des quatre texels les plus proches du pixel à afficher. Pour cela, rappelez-vous ce qui a été dit plus haut : les coordonnées x,y d'un pixel ont une partie entière et une partie fractionnaire. Le filtrage au plus proche élimine les parties fractionnaires, ce qui donne une coordonnée x,y. Avec le filtrage bilinéaire, on prend les texels de coordonnées (x,y) ; (x+1,y) ; (x,y+1) ; (x+1,y+1), le pixel étant entre ces 4 texels.
Mais le filtrage ne fait pas qu'une simple moyenne, il prend en compte les parties fractionnaires pour faire la moyenne. En effet, le pixel n'est pas au milieu du carré de texel, il est quelque part mais est souvent plus proche d'un texel que des autres. Et il faut donc pondérer la moyenne par les distances aux 4 texels. Pour cela, la moyenne est calculée à partir d'interpolations linéaires. Avec 4 pixels, nous allons devoir calculer la couleur de deux points intermédiaires. La couleur de ces deux points se calcule par interpolation linéaire, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.
[[File:Bilin3.png|centre|vignette|upright=2|Filtrage bilinéaire de texture.]]
Le circuit qui permet de faire l'interpolation bilinéaire est particulièrement simple. On trouve un circuit de chaque pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu, et un pour la transparence. Chacun de ces circuit est composé de sous-circuits chargés d'effectuer une interpolation linéaire, reliés comme suit.
[[File:Texture sampler unit.png|centre|vignette|Unité de filtrage bilinéaire.]]
Vous noterez que le filtrage bilinéaire accède à 4 pixels en même temps. Fort heureusement, les textures sont stockées de manière à ce qu'on puisse charger les 4 pixels en une fois, comme on l'a vu plus haut. Le filtrage bilinéaire a de fortes chances que les 4 pixels filtrés soient dans la même ''tile'', la seule exception étant quand ils sont tout juste sur le bord d'une ''tile''.
: La console de jeu Nintendo 64 n'utilise que trois pixels au lieu de quatre dans son interpolation bilinéaire, qui en devient une interpolation quasi-bilinéaire. La raison derrière ce choix est une question de performances, comme beaucoup de décisions de ce genre. Le résultat est un rendu imparfait de certaines textures.
===Le filtrage trilinéaire===
Avec le filtrage bilinéaire, des discontinuités apparaissent sur certaines surfaces. Par exemple, pensez à une texture de sol : elle est appliquée plusieurs fois sur toute la surface du sol. A une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512*512 à 256*256, ce qui est visible pour un joueur attentif. De telles transitions sont lissées grâce au filtrage linéaire, il n'y a plus qu'à le combiner avec le filtrage bilinéaire. Rien d’incompatible : le premier filtre l'intérieur d'une mip-map, le second combine deux mip-maps.
Le filtrage trilinéaire prend les deux mip-maps les plus proches, fait un filtrage bilinéaire avec chacune, puis fait une « une moyenne » pondérée entre les deux résultats. Le circuit de filtrage trilinéaire existe en plusieurs versions. La plus simple, illustrée ci-dessous, effectue deux filtrages bilinéaires en parallèle, dans deux circuits séparés, puis combine leurs résultats avec un circuit d'interpolation linéaire. Mais ce circuit nécessite de charger 8 texels simultanément. Qui plus est, ces 8 texels ne sont pas consécutifs en mémoire, car ils sont dans deux niveaux de détails/mip-maps différents.
[[File:Parallel trilinear filtering.png|centre|vignette|upright=2.0|Unité de filtrage trilinéaire parallèle.]]
Vu qu'on lit des texels dans deux mip-maps, les texels sont lus en deux fois : 4 texels provenant de la première mip-map, suivis par les 4 texels de l'autre mip-map. Les 4 premiers texels doivent donc être mis en attente dans des registres, en attendant que les 4 autres arrivent. Une amélioration du circuit précédent gère cela en ajoutant des registres. Il lit les 4 premiers texels, les filtre avec une interpolation bilinéaire, et mémorise le résultat dans un registre. Puis, il lit les 4 autres texels, les filtre, et met le résultat dans un second registre. A ce moment là, un circuit d'interpolation linéaire finit le travail. On économise donc un circuit d'interpolation bilinéaire, sans que les performances soient trop impactées.
[[File:Filtrage trilineaire.png|centre|vignette|upright=1.0|Unité de filtrage trilineaire série.]]
Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le temps où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. À chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients, et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.
===Le filtrage anisotrope===
D'autres artefacts peuvent survenir lors de l'application d'une texture, la perspective pouvant déformer les textures et entraîner l'apparition de flou. La raison à cela est que les techniques de filtrage de texture précédentes partent du principe que la texture est vue de face. Prenez une texture carrée, par exemple. Vue de face, elle ressemble à un carré sur l'écran. Mais tournez la caméra, de manière à voir la texture de biais, avec un angle, et vous verrez que la forme de la texture sur l'écran est un trapèze, pas un carré. Cette déformation liée à la perspective n'est pas prise en compte par les méthodes de filtrage de texture précédentes. Pour le dire autrement, les techniques de filtrage précédentes partent du principe que les 4 texels qui entourent un pixel forment un carré, ce qui est vrai si la texture est vue de face, sans angle, mais ne l'est pas si la texture n'est pas perpendiculaire à l'axe de la caméra. Du point de vue de la caméra, les 4 texels forment un trapèze d'autant moins proche d'un carré que l'angle est grand.
Pour corriger cela, les chercheurs ont inventé le '''filtrage anisotrope'''. En fait, je devrais plutôt dire : LES filtrages anisotropes. Il en existe un grand nombre, dont certains ne sont pas utilisés dans les cartes graphiques actuelles, soit car ils trop gourmand en accès mémoires et en calculs pour être efficaces, soit car ils ne sont pas pratiques à mettre en œuvre. Il est très difficile de savoir quelles sont les techniques de filtrage de texture utilisées par les cartes graphiques, qu'elles soient récentes ou anciennes. Beaucoup de ces technologies sont brevetées ou gardées secrètes, et il faudrait vraiment creuser les brevets déposés par les fabricants de GPU pour en savoir plus. Les algorithmes en question seraient de plus difficiles à comprendre, les méthodes mathématiques cachées derrière ces méthodes de filtrage n'étant pas des plus simple.
[[File:Anisotropic filtering en.png|centre|vignette|upright=2|Exemple de filtrage anisotrope.]]
==La compression de textures==
Les textures les plus grosses peuvent aller jusqu'au mébioctet, ce qui est beaucoup. Pour limiter la casse, les textures sont compressées. La '''compression de texture''' réduit la taille des textures, ce qui peut se faire avec ou sans perte de qualité. Elle entraîne souvent une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande. Mais il s'agit là d'une technique très simple, beaucoup plus simple que les techniques que nous allons voir dans cette section. Nous allons voir quelque algorithmes de compression de textures de complexité intermédiaire, mais n'allons pas voir l'état de l'art. Il existe des formats de texture plus récents que ceux qui nous allons aborder, comme l{{'}}''Ericsson Texture Compression'' ou l{{'}}''Adaptive Scalable Texture Compression'', plus complexes et plus efficaces.
Notons que les textures sont compressées dans les fichiers du jeu, mais aussi en mémoire vidéo. Les textures sont décompressées lors de la lecture. Pour cela, la carte graphique contient alors un circuit, capable de décompresser les textures lorsqu'on les lit en mémoire vidéo. Les cartes graphiques supportent un grand nombre de formats de textures, au niveau du circuit de décompression. Du fait que les textures sont décompressées à la volée, les techniques de compression utilisées sont assez particulières. La carte graphique ne peut pas décompresser une texture entière avant de pouvoir l'utiliser dans un ''pixel shader''. A la place, on doit pouvoir lire un morceau de texture, et le décompresser à la volée. On ne peut utiliser les méthodes de compression du JPEG, ou d'autres formats de compression d'image. Ces dernières ne permettent pas de décompresser une image morceau par morceau.
Pour permettre une décompression/compression à la volée, les textures sont des textures tilées, généralement découpées en tiles de 4 * 4 texels. Les ''tiles'' sont compressées indépendamment les unes des autres. Et surtout, avec ou sans compression, la position des tiles en mémoire ne change pas. On trouve toujours une tile tous les T octets, peu importe que la tile soit compressée ou non. Par contre, une tile compressée n'occupera pas T octets, mais moins, là où une tile compressée occupera la totalité des T octets. En clair, compresser une tile fait qu'il y a des vides entre deux tiles dans al mémoire vidéo, mais ne change rien à leur place en mémoire vidéo qui est prédéterminée, peu importe que la texture soit compressée ou non. L'intérêt de la compression de textures n'est pas de réduire la taille de la texture en mémoire vidéo, mais de réduire la quantité de données à lire/écrire en mémoire vidéo. Au lieu de lire T octets pour une tile non-compressée, on pourra en lire moins.
===La palette indicée et la technique de ''Vector quantization''===
La technique de compression des textures la plus simple est celle de la '''palette indicée''', que l'on a entraperçue dans le chapitre sur les cartes d'affichage. La technique de '''''vector quantization''''' peut être vue comme une amélioration de la palette, qui travaille non pas sur des texels, mais sur des ''tiles''. À l'intérieur de la carte graphique, on trouve une table qui stocke toutes les ''tiles'' possibles. Chaque ''tile'' se voit attribuer un numéro, et la texture sera composé d'une suite de ces numéros. Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l’embarqué utilisent ce genre de compression.
===Les algorithmes de ''Block Truncation coding''===
La première technique de compression élaborée est celle du '''''Block Truncation Coding''''', qui ne marche que pour les images en niveaux de gris. Le BTC ne mémorise que deux niveaux de gris par ''tile'', que nous appellerons couleur 1 et couleur 2, les deux niveaux de gris n'étant pas le même d'une ''tile'' à l'autre. Chaque pixel d'une ''tile'' est obligatoirement colorié avec un de ces niveaux de gris. Pour chaque pixel d'une ''tile'', on mémorise sa couleur avec un bit : 0 pour couleur 1, et 1 pour couleur 2. Chaque ''tile'' est donc codée par deux entiers, qui codent chacun un niveau de gris, et une suite de bits pour les pixels proprement dit. Le circuit de décompression est alors vraiment très simple, comme illustré ci-dessous.
[[File:Block Truncation coding.jpg|centre|vignette|upright=2.0|Block Truncation coding.]]
La technique du BTC peut être appliquée non pas du des niveaux de gris, mais pour chaque composante Rouge, Vert et Bleu. Dans ces conditions, chaque ''tile'' est séparée en trois sous-''tiles'' : un sous-bloc pour la composante verte, un autre pour le rouge, et un dernier pour le bleu. Cela prend donc trois fois plus de place en mémoire que le BTC pur, mais cela permet de gérer les images couleur.
===Le format de compression S3TC / DXTC===
L'algorithme de '''Color Cell Compression''', ou CCC, améliore le BTC pour qu'il gère des couleurs autre que des niveaux de gris. Ce CCC remplace les deux niveaux de gris par deux couleurs. Une ''tile'' est donc codée avec un entier 32 bits par couleur, et une suite de bits pour les pixels. Le circuit de décompression est identique à celui utilisé pour le BTC.
[[File:Color Cell Compression.jpg|centre|vignette|Color Cell Compression.]]
[[File:Dxt1-memory-layout.png|vignette|Dxt1 et ''color cell compression''.]]
Le format de compression de texture utilisé de base par Direct X, le DXTC, est une version amliorée de l'algorithme précédent. Il est décliné en plusieurs versions : DXTC1, DXTC2, etc. La première version du DXTC est une sorte d'amélioration du CCC : il ajoute une gestion minimale de transparence, et découpe la texture à compresser en ''tiles'' de 4 pixels de côté. La différence, c'est que la couleur finale d'un texel est un mélange des deux couleurs attribuée au bloc. Pour indiquer comment faire ce mélange, on trouve deux bits de contrôle par texel.
Si jamais la couleur 1 < couleur2, ces deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (2 * Couleur1 + Couleur2) / 3
* 11 = (Couleur1 + 2 * Couleur2) / 3
Sinon, les deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (Couleur1 + Couleur2) / 2
* 11 = Transparent
[[File:DXTC.jpg|centre|vignette|DXTC.]]
Le circuit de décompression du DXTC ressemble alors à ceci :
[[File:Circuit de décompression du DXTC.jpg|centre|vignette|upright=2.0|Circuit de décompression du DXTC.]]
===Les format DXTC 2, 3, 4 et 5 : l'ajout de la transparence===
Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3, lui-même replacé par le DXT4 et par le DXT5. Dans le DXT3, la transparence fait son apparition. Pour cela, on ajoute 64 bits par ''tile'' pour stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1.
[[File:Dxt23-memory-layout.png|centre|vignette|Dxt 2 et 3.]]
Dans le DXT4 et le DXT5, la méthode utilisée pour compresser les couleurs l'est aussi pour les valeurs de transparence. L'information de transparence est stockée par un en-tête contenant deux valeurs de transparence, le tout suivi d'une matrice qui attribue trois bits à chaque texel. En fonction de la valeur des trois bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1.
[[File:Dxt45-memory-layout.png|centre|vignette|Dxt 4 et 5.]]
===Le format de compression PVRTC===
Passons maintenant à un format de compression de texture un peu moins connu, mais pourtant omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle travaille surtout dans les cartes graphiques embarquées. Ses cartes se trouvent notamment dans l'ipad, l'iPhone, et bien d'autres smartphones actuels.
Avec le PVRTC, les textures sont encore une fois découpées en ''tiles'' de 4 texels par 4, mais la ressemblance avec le DXTC s’arrête là. Chacque ''tile'' est codée avec :
* une couleur codée sur 16 bits ;
* une couleur codée sur 15 bits ;
* 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
* et un bit de modulation, qui permet de configurer l’interprétation des bits de mélange.
Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4*4.
==Annexe : le ''normal-mapping'' hardware==
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, parlons un peu du ''normal mapping'' et de son implémentation dans les unités de texture. Pour rappel, 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. Et pour comprendre quel est le rapport avec les textures, nous allons devoir faire quelques rappels sur l'éclairage par pixel.
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
L'éclairage se fait en utilisant de nombreux calculs, qu'on a détaillé dans le chapitre sur les bases du rendu 3D. Ceux-ci utilisent la normale d'un sommet, à savoir un vecteur orienté à la verticale de la surface. En théorie, il y a une normale par sommet, ce qui fait que les calculs d'éclairage doivent se faire au niveau géométrique, avant la rastérisation. Mais pour obtenir un éclairage de meilleure qualité, il y a des techniques qui calcule l'éclairage d'une scène 3D pixel par pixel.
Une première technique d'éclairage par pixel interpole les normales lors de l'étape de rastérisation. On obtient alors un éclairage de Phong. Une autre solution est celle du ''normal mapping''. Le '''''normal mapping''''' précalcule les normales d'une surface dans une texture, appelée la ''normal map''. L'éclairage est alors réalisé par un pixel shader, qui lit les normales depuis cette texture et fait les calculs d'éclairage avec.
===L'usage de textures non-compressées pour les ''normal maps''===
La ''normal map'' est une texture, donc. Mais ce n'est pas une texture comme une autre. Elle mémorise un vecteur pour chaque texel, pas une couleur. Il est possible d'utiliser les formats de textures non-compressés, même si cela ne donne pas de bons résultats. Après tout, un vecteur est codé par trois coordonnées x,y,z, il est possible de coder chacune avec un octet et de packager cela dans une texture RGB classique. La coordonnée x va dans la composante Rouge, la coordonnée Y dans la composante Bleu, et la coordonnée z dans la composante z.
Une première optimisation est de ne pas mémoriser les trois coordonnées, mais seulement deux d'entre elles et de calculer la troisième. En effet, les normales sont des vecteurs ''normalisés'', c'est à dire que leur longueur vaut 1, par construction. Vu que la taille est connue à l'avance, on peut en déduire la coordonnée z à partir des coordonnées x et y, avec l'usage du théorème de Pythagore. Par définition, <math>z^2 = 1 - x^2 - y^2</math>. Le calcul peut être fait dans le ''pixel shader'' sans problème.
: Pour être précis, il faut que les normales soient définies d'une certaine manière pour que ça marche. Les normales ne sont pas définies dans le même système de coordonnées que le modèle 3D, mais dans un autre système appelé l'''espace tangent''.
Notons que l'accès à la ''normal map'' se fait comme pour n'importe quelle texture : les texels sont lus en mémoire vidéo, puis le filtrage de texture est appliqué, et enfin le tout est envoyé au ''pixel shader''. En théorie, le filtrage effectue une sorte d'interpolation des normales automatique, ce qui permet de trouver la normale à un pixel précis, même s'il n'est pas sur un texel.
Les solutions précédentes demandent d'utiliser des textures non-compressées, qui utilisent beaucoup de mémoire vidéo. Utiliser des textures compressées pourrait sembler résoudre le problème. Mais l'usage de textures compressées marche très mal pour les ''normal maps'', elles ne permettent pas d'obtenir une qualité ou une compression suffisante. Aussi, des formats de texture dédiés aux ''normal maps'' ont été inventés.
===Le format de texture 3Dc===
Le problème est que le rendu final n'est pas très beau. Et les opérations de filtrage ne donnent pas de très bons résultats. Pour remédier à ces problèmes, des formats de texture spécialisés pour les ''normal map'' ont été inventés. Le premier d'entre eux est le '''3Dc''', aussi appelé BC5 pour ''Block Compression 5''. Il utilise un octet par texel, au lieu de trois avec une texture RGB normale non-compressée. Et surtout : il est géré en matériel, directement dans l'unité de texture, qui peut décompresser les textures 3Dc et les filtrer !
Avec le 3Dc, seules coordonnées sont mémorisées, la troisième est calculée, comme dit plus haut. Pour le reste, la compression de la ''normal map'' ressemble un peu à celle des textures. La ''normal map'' est découpée en blocs de 4 par 4 texels de côté, chacune étant encodée en tenant compte de certaines redondances. Dans un bloc, les coordonnées x sont dans un certain intervalle, allant de <math>x_{min}</math> à <math>x_{max}</math>, idem pour la coordonnée y qui est dans l'intervalle <math>y_{min}</math>, <math>y_{max}</math>. L'intervalle <math>x_{min}</math> à <math>x_{max}</math> est coupé en 8 parts égales, ce qui fait 8 valeurs possibles dans cet intervalle, chacune correspondant à une valeur x possible pour une normale du bloc. La normale est donc encodée en précisant quelle valeur est la bonne, en utilisant un indice de 3 bits pour cela. La valeur exacte de la coordonnée x se calcule à partir de l'indice comme suit :
: <math>x = x_{min} + \left( \frac{x_{min} - x_{min}}{8} \right) \times \text{indice} </math>
Pour encoder un bloc de 4 par 4 normales, il faut donc :
* Un octet pour chaque coordonnée <math>x_{min}</math>, <math>x_{max}</math>, <math>y_{min}</math>, <math>y_{max}</math>.
* 6 bits par normale : 3 pour encoder le décalage de la coordonnée x, 3 pour la coordonnée y.
Le tout permet d'encoder un bloc de 16 normales en seulement 128 bits, soit un octet par normale. Le ''pixel shader'' se débrouille pour décompresser un bloc. Il effectue notamment le calcul du dessus, pour retrouver la coordonnée x. Les calculs étant particulièrement simples, le cout en performance est très faible. Et ce d'autant plus qu'ils demandent juste de faire des additions et des multiplications entières, que l'unité scalaire peut faire en parallèle d'autres opérations plus gourmandes. Et le cout en calculs est de toute façon compensé par l'économie de mémoire et de bande passante lié à la compression : diviser la taille des données par trois, ca a un sacré impact !
==Annexe : les ''shadowmap'' hardware==
Les anciens GPU, notamment la Geforce FX, avaient des fonctionnalités spécifiques pour le calcul des ombres. Dans la plupart des jeux vidéos de l'époque, et même de maintenant, les ombres sont calculées avec la technique des ''shadowmap''. L'idée est assez simple sur le principe : un pixel est dans l'ombre quand il est invisible depuis une source de lumière.
L'idée est que le rendu est réalisé en plusieurs passes, avec une passe par source de lumière et une passe finale pour calculer l'image finale. Nous allons expliquer la technique avec une seule source de lumière, et allons utiliser l'exemple de la scène ci-dessous.
[[File:7fin.png|centre|vignette|Scène 3D d'exemple.]]
===La technique du ''shadowmapping''===
[[File:2shadowmap.png|vignette|Résultat de la première passe : ''shadowmap''..]]
La première passe rend l'image depuis le point de vue de la source de lumière. Cette première passe ne rend pas les couleurs de la scène, elle ne s'intéresse qu'à la profondeur des pixels. Le résultat est que l'image ne rend que le tampon de profondeur. Celui-ci est ensuite réutilisé comme texture pour la passe suivante. La texture en question est appelée la '''''shadownmap'''''.
La perspective utilisée, ainsi que le ''view frustrum'', dépend de la source de lumière. Pour une source de lumière qui émet un cône de lumière, le ''view frustrum'' de l'image rendue doit contenir tout le cone de lumière, et doit coller le plus possible à celui-ci. Pour une source directionnelle, comme le soleil, une perspective orthographique est utilisée.
La seconde passe rend l'image du point de vue de la caméra, pour rendre l'image finale. Elle rend l'image finale, qui est composée de pixels, chacun ayant une position à l'écran x,y, et une profondeur z. Les coordonnées sont transformées pour obtenir la position de ce pixel depuis le point de vue de la caméra. Une simple multiplication de matrice suffit, rien de bien compliqué, un shader peut le faire.
[[File:5failed.png|vignette|Résultat du test des comparaisons.]]
Après cette étape, on a alors les coordonnées x,y,z de ce pixel du point de vue de la caméra, et la ''shadowmap''. Il est alors possible d'accéder à la ''shadowmap'' au même endroit, à la même place que le pixel testé, aux mêmes coordonnées x,y. Si la profondeur du pixel est supérieure à celle de la shadowmap au même endroit, alors le pixel est situé derrière la surface visible, donc est dans l'ombre. Sinon, il n'est pas dans l'ombre. Le même procédé est répété sur chaque pixel de l'écran.
===Les optimisations hardware du ''shadowmapping''===
La technique des ''shadowmap'' demande donc de calculer une texture ''shadowmap'', puis de lire celle-ci et de faire des comparaisons de profondeur. Les GPU comme la Geforce FX intégraient du matériel dans les unités de texture pour faciliter ce travail. Les unités de texture pouvaient lire les ''shadowmap'', et faire la comparaison de profondeur toutes seules, elles avaient des circuits pour. Il suffisait de leur fournir le pixel à tester, ses coordonnées x,y,z, et l'adresse de la ''shadowmap''. Les unités de texture renvoyaient alors un résultat valant 0 ou 1 : 1 si le pixel est dans l'ombre, 0 sinon.
Elles pouvaient même effectuer du filtrage de texture sur les ''shadowmap''. Mais le filtrage était différent de celui utilisé sur les autres textures : moyenner des valeurs de profondeur ne marche pas bien. Elles utilisaient des techniques de filtrage différentes : elles faisaient les tests de comparaison, puis faisaient la moyenne des résultats. Ainsi, pour du filtrage bilinéaire, elles lisaient 4 texels dans la ''shadowmap'', puis faisaient 4 tests de comparaison, et moyennaient les 4 résultats.
==Annexe : le cube-mapping==
[[File:Cube mapped reflection example 2.JPG|vignette|Exemple de reflets environnementaux.]]
L''''environnement-mapping''' est une technique de calcul de divers effets graphiques liés à l'environnement, notamment des réflexions. L'idée est de plaquer une texture pré-calculée pour simuler l'effet de l'environnement sur une surface ou un objet 3D. Il en existe plusieurs versions différentes, mais la seule utilisée de nos jours est le ''cube-mapping'', où la texture de l'environnement est plaquée sur un cube, d'où son nom. Le cube en question est utilisé différemment suivant ce que l'on cherche à faire avec le ''cube-mapping''.
===Les utilisations du ''cubemapping''===
Les deux utilisations principales sont le rendu du ciel et des décors, et les réflexions sur la surface des objets. Dans les deux cas, l'idée est de précalculer ce que l'on voit du point de vue de la caméra. On place la caméra dans la scène 3D, on place un cube centré sur la caméra, le cube est texturé avec ce que l'on voit de l'environnement depuis la caméra/l'objet de son point de vue.
[[File:Panorama cube map.png|centre|vignette|upright=2|L'illustration montre en premier lieu une ''cubemap'' avec les six faces mises en évidence, puis quel environnement 3D elle permet de simuler, le troisième illustration montrant comment la ''cubemap'' est utilisée pour simuler l'environnement.]]
Le rendu du ciel et des décors lointains dans les jeux vidéo se base sur des '''''skybox''''', à savoir un cube centré sur la caméra, sur lequel on ajoute des textures de ciel ou de décors lointains. Le cube est recouvert par une texture, qui correspond à ce que l'on voit quand on dirige le regard de la caméra vers cette face. Contrairement à ce qu'on pourrait croire, la skybox n'est pas les limites de la scène 3D, les limites du niveau d'un jeu vidéo ou quoique ce soit d'autre de lié à la physique de la scène 3D. La skybox est centrée sur la caméra, elle suit la caméra dans son mouvement. Centrer la skybox sur la caméra permet de simuler des décors très lointains, suffisamment lointain pour qu'on n'ait pas l'illusion de s'en rapprocher en se déplaçant dans la map. De plus, cela évite d'avoir à faire trop de calculs à chaque fois que l'on bouge la caméra. La texture plaquée sur le cube est une texture unique, elle-même découpée en six sous-textures, une par face du cube.
[[File:Skybox example.png|centre|vignette|upright=2|Exemple de Skybox.]]
[[File:Cube mapped reflection example.jpg|vignette|Réflexions calculées par une ''cubemap''.]]
Le ''cube-mapping'' est aussi utilisé pour des reflets. L'idée est de simuler les reflets en plaquant une texture pré-calculée sur l'objet réflecteur. La texture pré-calculée est un dessin de l'environnement qui se reflète sur l'objet, un dessin du reflet à afficher. En la plaquant la texture sur l'objet, on simule ainsi des reflets de l'environnement, mais on ne peut pas calculer d'autres reflets comme les reflets objets mobiles comme les personnages. Et il se trouve que la texture pré-calculée est une ''cubemap''. Pour les environnements ouverts, c'est la ''skybox'' qui est utilisée, ce qui permet de simuler les reflets dans les flaques d'eau ou dans des lacs/océans/autres. Pour les environnements intérieurs, c'est une cubemap spécifique qui utilisée. Par exemple, pour l'intérieur d'une maison, on a une ''cubemap'' par pièce de la maison. Les reflets se calculent en précisant quelle ''cubemap'' appliquer sur l'objet en fonction de la direction du regard.
[[File:Cube map level.png|centre|vignette|Cube map de l'intérieur d'une pièce d'un niveau de jeux vidéo.]]
===L'implémentation matérielle du ''cubemapping''===
Toujours est-il que les textures utilisées pour le ''cubemapping'', appelées des ''cubemaps'', sont en réalité la concaténation de six textures différentes. En mémoire vidéo, la ''cubemap'' est stockée comme six textures les unes à la suite des autres. Lors du rendu, on doit préciser quelle face du cube utiliser, ce qui fait 6 possibilités. On a le même problème qu'avec les niveaux de détail, sauf que ce sont les faces d'une ''cubemap'' qui remplacent les textures de niveaux de détails. L'accès en mémoire doit donc préciser quelle portion de la ''cubemap'' il faut accéder. Et l'accès mémoire se complexifie donc. Surtout que l'accès en question varie beaucoup suivant l'API graphique utilisée, et donc suivant la carte graphique.
Le support des ''cubemaps'' dépend de l'API 3D, et surtout de si elle date ou si elle est récente :
* Les API 3D très anciennes ne gérent pas nativement les ''cubemaps'', qui doivent être émulées en logiciel en utilisant six textures différentes. Le pixel shader décide donc quelle ''cubemap'' utiliser, avec quelques calculs sur la direction du regard.
* Les API 3D récentes gèrent nativement les ''cubemaps''. Pour les versions les plus vielles de ces API, les six faces sont numérotées et l'accès à une ''cubemap'' précise quel face utiliser en donnant son numéro. La carte graphique choisit alors automatiquement la bonne texture. Mais cela demande de laisser le calcul de la bonne face au pixel shader.
* Dans les API 3D modenres, les ''cubemap'' sont gérées comme des textures en trois dimensions, adressées avec trois coordonnées u,v,w. La carte graphique utilise ces trois coordonnées de manière à en déduire quelle est la face pertinente, mais aussi les coordonnées u,v dans la texture de la face.
==Annexe : les textures virtuelles==
Les '''textures virtuelles''' sont une optimisation des textures normales, qui visent à accélérer le rendu de terrains de grande taille. Imaginez par exemple un monde assez ouvert, comme un environnement en forêt ou en montagne, avec une grande distance de visibilité. Avec de tels terrains, le "sol" est recouvert par une texture de sol unique qui recouvre tout le terrain. Elle ne se répète pas, est de très grande taille, et peut parfois recouvrir toute la map ! Mais il n'y a pas assez de mémoire vidéo pour mémoriser la texture toute entière. La seule solution est la suivante : une partie de la texture est placée en mémoire vidéo, le reste est soit placé en mémoire RAM ou sur le disque dur.
Pour cela, le moteur de jeu utilise une optimisation ingénieuse, basée sur une observation assez basique : une bonne partie de la texture est visible, mais le reste est caché par des arbres, des habitations ou d'autres obstacles. Une optimisation possible de ne garder en mémoire vidéo que les portions visibles de la texture, pas les portions cachées. Une autre optimisation mélange textures virtuelles et ''mip-mapping''. L'idée est que pour les portions lointaines d'une texture, la texture utilisée est une ''mip-map'' de basse résolution. L'idée est alors de ne charger que la ''mip-map'' adéquate, pas les autres niveaux de détail. En clair, la texture de base n'est pas chargée en mémoire vidéo, mais la ''mip-map'' basse résolution l'est.
===Une texture à deux niveaux===
L'implémentation des textures virtuelles découpe les méga-textures en ''tiles'', en morceaux rectangulaires de taille modeste. En clair, le terrain est découpé en morceau rectangulaires/carrés. Seules les tiles nécessaires sont chargées en mémoire vidéo, pas les autres. Par exemple, les ''tiles'' non-visibles ne sont pas placées en mémoire vidéo, seules les ''tiles'' visibles le sont. De même, il y a une ''tile'' par niveau de mip-map : seul la tile correspondant le niveau adéquat est en mémoire vidéo, les autres niveaux de détail ne sont pas chargés. On peut faire une analogie avec la mémoire virtuelle, où les données sont découpées en pages, qui sont chargées en mémoire RAM à la demande, suivant les besoins, les données pouvant être swappées sur le disque dur si elles sont peu utilisées. Sauf qu'ici, il s'agit de textures qui sont découpées en pages chargées à la demande en mémoire vidéo, depuis la RAM système.
Une texture virtuelle est en réalité un système à deux niveaux : une liste de ''tiles'' et les ''tiles'' elles-mêmes. La liste de ''tiles'' est appelée un '''atlas de texture''', c'est un peu l'équivalent de la ''tilemap'' pour le rendu 2D. Rendre une texture demande de calculer quelle ''tile'' contient le texel à afficher, consulter la ''tile'' en question, puis récupérer le texel adéquat dans cette ''tile''. La ''tile'' est donc une texture, mais la texture à charger est choisie parmi un ensemble, qui est ici l'atlas de texture.
===L'implémentation : logicielle versus matérielle===
Les textures virtuelles ont été utilisées pour la première fois par les jeux Rage 1 et 2 d'IdSoftware, et quelques jeux ultérieurs comme DOOM 2016. IdSoftware les appelait des '''''mega-textures'''''. L'optimisation permettait des gains en performance assez impressionnants. Le jeu Rage 1 utilisait une texture carrée unique de 128k pixels de côté pour rendre le terrain. En théorie, une telle texture devrait prendre 64 giga-octets, mais le jeu tournait correctement avec 512 méga-octets de RAM, poussivement avec seulement 256 méga-octets de RAM.
De nos jours, les textures virtuelles sont supportées par beaucoup de jeux vidéos, les moteurs les plus courants gèrent de telles textures de manière logicielles. Mais quelques GPU récents supportent les textures virtuelles. Sur les GPU récents, l'atlas de texture est géré nativement par le matériel. Le GPU choisit quelle ''tile'', quelle texture choisir pour rendre le texel adéquat. Pour cela, le GPU calcule quelle ''tile'' charger, consulte l'atlas de texture, et lit la texture de ''tile'' adéquate.
Mais l'implémentation sur les GPU récents a de nombreuses limitations. La limitation la plus importante est que la taille des textures virtuelles ne peut pas dépasser la taille d'une texture normale, soit 32768 pixels de côté pour une texture carrée environ sur les GPU de 2020. De plus, le chargement d'une ''tile'' est très lent. En clair, dès qu'on veut changer de niveau de mip-map pour une tile, ou dès qu'une tile devient visible, le chargement de la tile peut facilement prendre plusieurs centaines de millisecondes. Le filtrage de texture est très complexe avec des textures virtuelles, ce qui fait que le filtrage de texture virtuelle est souvent soumis à des limitations que les textures normales n'ont pas, notamment pour le filtrage anisotropique.
{{NavChapitre | book=Les cartes graphiques
| prev=Le rasterizeur
| prevText=Le rasterizeur
| next=Les Render Output Target
| nextText=Les Render Output Target
}}{{autocat}}
ipblnk522iiy4yrssdm4jinvsepqsis
763424
763423
2026-04-10T19:32:51Z
Mewtow
31375
/* Annexe : le cube-mapping */
763424
wikitext
text/x-wiki
[[File:Texture mapping.png|vignette|''Texture mapping'']]
Les '''textures''' sont des images que l'on va plaquer sur la surface d'un objet, du papier peint en quelque sorte. Les cartes graphiques supportent divers formats de textures, qui indiquent comment les pixels de l'image sont stockés en mémoire : RGB, RGBA, niveaux de gris, etc. Une texture est donc composée de "pixels", comme toute image numérique. Pour bien faire la différence entre les pixels d'une texture, et les pixels de l'écran, les pixels d'une texture sont couramment appelés des ''texels''.
==Le placage de textures inverse==
Pour rappel, plaquer une texture sur un objet consiste à attribuer un texel à chaque sommet, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture.
Dans les faits, on n'utilise pas de coordonnées entières de ce type. Les coordonnées de texture sont deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. Le nom donnée à cette technique de description des coordonnées de texture s'appelle l''''''UV Mapping'''''.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Les API 3D modernes gèrent des textures en trois dimensions, ce qui ajoute une troisième coordonnée de texture notée w. Dans ce qui va suivre, nous allons passer les textures en trois dimensions sous silence. Elles ne sont pas très utilisées, la quasi-totalité des jeux vidéo et applications 3D utilisant des textures en deux dimensions. Par contre, le matériel doit gérer les textures 3D, ce qui le rend plus complexe que prévu. Il faut ajouter quelques circuits pour, de quoi gérer la troisième coordonnée de texture, etc.
Lors de la rastérisation, chaque fragment se voit attribuer un sommet, et donc la coordonnée de texture qui va avec. Si un pixel est situé pile sur un sommet, la coordonnée de texture de ce sommet est attribuée au pixel. Si ce n'est pas le cas, la coordonnée de texture finale est interpolée à partir des coordonnées des trois sommets du triangle rastérisé. L'interpolation en question a lieu dans l'étape de rastérisation, comme nous l'avons vu dans le chapitre précédent. Le fait qu'il y ait une interpolation fait que les coordonnées du pixel gagent à être des nombres flottants. On pourrait faire une interpolation avec des coordonnées de texture entières, mais les arrondis et autres imprécisions de calcul donneraient un résultat graphiquement pas terrible, et empêcheraient d'utiliser les techniques de filtrage de texture que nous verrons dans ce chapitre.
À partir de ces coordonnées de texture, la carte graphique calcule l'adresse du texel qui correspond, et se charge de le lire. Et toute la magie a lieu dans ce calcul d'adresse, qui part de coordonnées de texture flottante, pour arriver à une adresse mémoire. Le calcul de l'adresse du texel se fait en plusieurs étapes, que nous allons voir ci-dessous. La première étape convertit les coordonnées flottantes en coordonnées entières, qui disent à quel ligne et colonne se trouve le texel voulu dans la texture. L'étape suivante transforme ces coordonnées x,y entières en adresse mémoire.
===La normalisation des coordonnées===
J'ai dit plus haut que les coordonnées de texture sont des coordonnées flottantes, comprises entre 0 et 1. Mais il faut savoir que les pixels shaders peuvent modifier celles-ci pour mettre en œuvre certains effets graphiques. Et le résultat peut alors se retrouver en-dehors de l'intervalle 0,1. C'est quelque chose de voulu et qui est traité par la carte graphique automatiquement, sans que ce soit une erreur. Au contraire, la manière dont la carte graphique traite cette situation permet d'implémenter des effets graphiques comme des textures en damier ou en miroir.
[[File:Clamp tile.jpg|vignette|Clamp tile]]
Il existe globalement trois méthodes très simples pour gérer cette situation, qui sont appelés des '''modes d'adressage de texture'''.
* La première méthode est de faire en sorte que le résultat sature. Si une coordonnée est inférieur à 0, alors on la remplace par un zéro. Si elle est supérieure à 1, on la ramène à 1. Avec cette méthode, tout se passe comme si les bords de la texture étaient étendus et remplissaient tout l'espace autour de la texture. Le tout est illustré ci-dessous. Ce mode d'accès aux textures est appelé le '''''clamp'''''.
* Une autre solution retire la partie entière de la coordonnée, elle coupe tout ce qui dépasse 1. Pour le dire autrement, elle calcule le résultat modulo 1 de la coordonnée. Le résultat est que tout se passe comme si la texture était répétée à l'infini et qu'elle pavait le plan.
* Une autre méthode remplit les coordonnées qui sortent de l’intervalle 0,1 avec une couleur préétablie, configurée par le programmeur.
===La conversion des coordonnées de textures flottantes en adresse mémoire===
Une fois la normalisation effectuée, les coordonnées de texture sont utilisées pour lire le texel voulu. Pour cela, les coordonnées de texte sont transformées en adresse mémoire, adresse qui pointe sur le texel ayant ces cordonnées. Pour cela, la première étape est de transformer les coordonnées flottantes u,v en coordonnées entières x,y qui pointent sur un texel. Pour cela, il suffit de multiplier les coordonnées flottantes u,v par la résolution de la texture accédée. Pour un écran de résolution <math>\text{height,width}</math>, le calcul est le suivant :
: <math>x = u \times \text{width}</math>
: <math>y = v \times \text{height}</math>
Le résultat est un nombre avec une partie entière et une partie fractionnaire. La partie entière des deux coordonnées donne la position x,y voulue, et la partie fractionnaire est conservée pour le filtrage de textures, mais passons cela sous silence pour le moment.
La seconde étape prend les coordonnées entières x,y et calcule l'adresse mémoire du texel. L'adresse dépend de la position de la texture en mémoire, précisément de son début, son premier texel, mais aussi de la position du texel par rapport au début de la texture. Et calculer cette position intra-texture dépend de la manière dont les texels sont stockés en mémoire.
====Les textures naïves====
Les programmeurs qui lisent ce cours s'attendent certainement à ce que la texture soit stockée en mémoire ligne par ligne, ou colonne par colonne. Cela veut dire que le premier pixel en partant d'en haut à gauche est stocké en premier, puis celui immédiatement à sa droite, puis celui encore à droite, et ainsi de suite. Une fois qu'on arrive à la fin d'une ligne, on passe à la ligne suivante, en-dessous. Cette organisation ligne par ligne s'appele l'organisation '''''row major order'''''. On peut faire pareil, mais colonne par colonne, ce qui donne le '''''column major order'''''.
[[File:Speicheranordnung Feld.svg|centre|vignette|upright=2|Row et column major order.]]
Maintenant, supposons que la texture commence à l'adresse <math>A_\text{texture}</math>, qui est l'adresse du premier texel. La texture a une résolution de <math>\text{width}</math> texels de large et <math>\text{height}</math> texels de haut. Par définition, les coordonnées X et Y des texels commencent à 0, ce qui fait que le pixel en haut à gauche a les coordonnées 0,0.
L'adresse du pixel se calcule comme suit :
: <math>A_\text{pixel} = A_\text{texture} + (\text{taille d'une ligne en octets} \times Y) + (\text{taille d'un texel en octets} \times X)</math>
La taille d'un pixel en mémoire est notée T. La taille d'une ligne en mémoire est de <math>width \times T</math>, par définition, vu qu'elle fait <math>width</math> texels. On a donc :
: <math>A_\text{pixel} = A_\text{texture} + (width \times T \times Y) + (T \times X)</math>
La formule se réécrit comme suit :
: <math>A_\text{pixel} = A_\text{texture} + T \times (width \times Y + X)</math>
Le calcul d'adresse est donc assez simple. Malheureusement, les textures ne sont pas stockées de cette manière en mémoire vidéo. En effet, elle se marie mal avec les opérations de filtrage de texture que nous allons voir dans ce qui suit. Le filtrage d'un texel dépend de ses voisins du dessus et du dessous. Le fait que la texture n'est pas forcément parcourue ligne par ligne fait que stocker une texture ligne par ligne n'est pas l'idéal.
De même, les textures sont déformées par la perspective. L'affichage de la texture ne se fait alors pas ligne par ligne, mais en parcourant la texture en diagonale, l'angle de la diagonale correspondant approximativement à l'angle que fait la verticale de la texture avec le regard. Vu qu'on ne connait pas à l'avance l'angle que fera la diagonale de parcours, on doit ruser.
====Les textures tilées====
Une première solution à ce problème est celle des '''textures tilées'''. Avec ces textures, l'image de la texture est découpée en ''tiles'', des rectangles ou en carrés de taille fixe, généralement des carrés de 4 pixels de côté. Les tiles ont une largeur et une longueur égales, afin de simplifier les calculs : on divise X et Y par le même nombre. De plus, leur largeur et leur longueur sont une puissance de deux, afin de simplifier les calculs d'adresse. Les ''tiles'' sont alors mémorisée les unes après les autres dans le fichier de la texture.
[[File:Texture tilée.png|centre|vignette|upright=2|Texture tilée]]
La formule de calcul d'adresse vue plus haut doit être adaptée pour tenir compte des tiles. Pour cela, il faut remplacer la taille d'un texel par la taille d'une tile, et que la largeur de la texture soit exprimée en nombre de tiles. De plus, on doit adapter les coordonnées des texels pour donner des coordonnées de tile. Généralement, les tiles sont des carrés de N pixels de côté, ce qui fait qu'on peut regrouper les lignes et les colonnes par paquets de N. Il suffit donc de diviser Y et X pour obtenir les coordonnées de la tile, de même que la larguer. La formule pour calculer la position de la énième tile est alors la suivante :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + \text{Taille mémoire d'une tile} \times \left( {\text{Width} \over N} \times {Y \over N} + {X \over N} \right)</math>
On peut réécrire le tout comme suit :
: <math>\text{adresse d'une tile} = \text{adresse du début de la texture} + K \times \left( {Y \over N} + X \right)</math>, avec K une constante connue à la compilation des shaders.
Vu que les tiles sont carrées avec une largeur qui est une puissance de deux, la multiplication par la taille d'une tile en mémoire se simplifie : on passe d'une multiplication entière à des décalages de bits. Même chose pour le calcul de l'adresse de la tile à partir des coordonnées x,y : ils impliquent des divisions par une puissance de deux, qui deviennent de simples décalages.
La position d'un pixel dans une tile dépend du format de la texture, mais peut se calculer avec quelques calculs arithmétiques simples. Dans les cas les plus simples, les pixels sont mémorisés ligne par ligne, ou colonne par colonne. Mais ce n'est pas systématiquement le cas. Toujours est-il que les calculs pour déterminer l'adresse sont simples, et ne demandent que quelques additions ou multiplications. Mais avec les formats de texture utilisés actuellement, les tiles sont chargées en entier dans le cache de texture, sans compter que diverses techniques de compression viennent mettre le bazar, comme on le verra dans la suite de cours.
Un avantage de l'organisation en tiles est qu'elle se marie bien avec le parcours des textures. On peut parcourir une texture dans tous les sens, horizontal, vertical, ou diagonal, on sait que les prochains pixels ont de fortes chances d'être dans la même tile. Si on rentre dans une tile par la gauche en haut, on a encore quelques pixels à parcourir dans la tile, par exemple. De même, le filtrage de textures est facilité. On verra dans ce qui va suivre que le filtrage de texture a besoin de lire des blocs de 4 texels, des carrés de 2 pixels de côté. Avec l'organisation en tile, on est certain que les 4 texels seront dans la même tile, sauf s'ils ont le malheur d'être tout au bord d'une tile. Ce dernier cas est assez rare, et il l'est d'autant plus que les tiles sont grandes. Enfin, un dernier avantage est que les tiles sont généralement assez petites pour tenir tout entier dans une ligne de cache. Le cache de texture est donc utilisé à merveille, ce qui rend les accès aux textures plus rapides.
====Les textures basées sur des ''z-order curves''====
Les formats de textures théoriquement optimaux utilisent une '''''Z-order curve''''', illustrée ci-dessous. L'idée est de découper la texture en quatre rectangles identiques, et de stocker ceux-ci les uns à la suite des autres. L'intérieur de ces rectangles est lui aussi découpé en quatre rectangles, et ainsi de suite. Au final, l'ordre des pixels en mémoire est celui illustré ci-dessous.
[[File:Z-CURVE.svg|centre|vignette|upright=2|Construction d'une ''Z-order curve''.]]
Les texels sont stockés les uns à la suite des autres dans la mémoire, en suivant l'ordre donnée par la ''Z-order curve''. Le calcul d'adresse calcule la position du texel en mémoire, par rapport au début de la texture, et ajoute l'adresse du début de la texture. Mais tout le défi est de calculer la position d'un texel en mémoire, à partir des coordonnées x,y. Le calcul peut sembler très compliqué, mais il n'en est rien. Le calcul demande juste de regarder les bits des deux coordonnées et de les combiner d'une manière particulièrement simple. Il suffit de placer le bit de poids fort de la coordonnée x, suivi de celui de la coordonnée y, et de faire ainsi de suite en passant aux bits suivants.
[[File:Zcurve45bits.png|centre|vignette|upright=1.5|Calcul de la position d'un élément dans une ''Z-order curve'' à partir des coordonnées x et y.]]
L'avantage d'une telle organisation est que la textures est découpées en ''tiles'' rectangulaires d'une certaine taille, elles-mêmes découpées en ''tiles'' plus petites, etc. Et il se trouve que cette organisation est parfaite pour le cache de texture. L'idéal pour le cache de texture est de charger une ''tile'' complète dans le cache de textures. Quand on accède à un texel, on s'assure que la ''tile'' complète soit chargée. Mais cela demande de connaitre à l'avance la taille d'une ''tile''. Les formats de texture fournissent généralement une ''tile'' carré de 4 pixels de côté, mais cela donnerait un cache trop petit pour être vraiment utile. Avec cette méthode, on s'assure qu'il y ait une ''tile'' avec la taille optimale. Les ''tiles'' étant découpées en ''tiles'' plus petites, elles-mêmes découpées, et ainsi de suite, on s'assure que la texture est découpées en ''tiles'' de taille variées. Il y aura au moins une ''tile'' qui rentrera tout pile dans le cache.
===Les techniques de rendu à textures multiples===
Nous venons de voir comment une texture est plaquée sur un objet 3D, ou une surface comme un sol. Pour résumer, le calcul de l'adresse d'un texel prend la position du texel par rapport au début de la texture, et ajoute l'adresse du début de la texture. L'adresse mémoire de la texture est connue au moment où le pilote de la carte graphique place la texture dans la mémoire vidéo, et cette information est transmise au matériel par l'intermédiaire du processeur de commande, puis passée aux processeurs de shaders et à l'unité de texture. Le tout est couplé à d'autres informations, la plus importante étant la ''taille de la texture en octets'', pour éviter de déborder lors des accès à la texture.
Néanmoins, il s'agit là du cas le plus simple. Certaines techniques de rendu demandent de choisir la texture à plaquer parmi un ensemble de plusieurs textures. Les techniques en question sont assez variées et n'ont pas grand chose en commun. Les plus connues sont le ''mip-mapping'', le ''cube-mapping'' et les textures virtuelles. Le ''mip-mapping'' sert à filtrer les textures, chose qu'on expliquera plus tard, le ''cube-mapping'' sert à simuler des réflexions sur un objet en plaquant une texture de l'environnement dessus, les textures virtuelles sont une optimisation pour les textures des terrains de grande taille. Mais malgré leurs différences, elles demandent de choisir quelle texture plaquer entre plusieurs textures de base. En clair, l'adresse de base de la texture varie selon la situation. Voyons-les dans le détail.
==L'implémentation matérielle du placage de textures==
Pour résumer, la lecture d'un texel demande d'effectuer plusieurs étapes. Dans le cas le plus simple, sans ''mip-mapping'' ou ''cubemapping'', on doit effectuer les étapes suivantes :
* Il faut d'abord normaliser les coordonnées de texture pour qu'elles tombent dans l'intervalle [0,1] en fonction du mode d'adressage désiré.
* Ensuite, les coordonnées u,v doivent être converties en coordonnées entières, ce qui demande une multiplication flottante.
* Enfin, l'adresse finale est calculée à partir des coordonnées entières et en ajoutant l'adresse de base de la texture (et éventuellement avec d'autres calculs arithmétiques suivant le format de la texture).
Tout cela pourrait être fait par le pixel shaders, mais cela implique beaucoup de calculs répétitifs et d'opérations arithmétiques assez lourdes, avec des multiplications flottantes, des additions et des multiplications entières, etc. Faire faire tous ces calculs par les shaders serait couteux en performance, sans compter que les shaders deviendraient plus gros et que cela aurait des conséquences sur le cache d'instruction. De plus, certaines de ces étapes peuvent se faire en parallèle, comme les deux premières, ce qui colle mal avec l'aspect sériel des shaders.
Aussi, les processeurs de shaders incorporent une unité de calcul d'adresse spéciale pour faire ces calculs directement en matériel. L'unité de calcul en question est dans l'unité de texture. Cette dernière contient donc au minimum deux circuits : un circuit de calcul d'adresse, et un circuit d'accès à la mémoire. Toute la difficulté tient dans le calcul d'adresse, plus que dans le circuit de lecture. Le calcul d'adresse est conceptuellement réalisé en deux étapes. La première étape qui transforme les coordonnées u,v en coordonnées x,y qui donne le numéro de la ligne et de la colonne du texel dans la texture. La seconde étape prend ces deux coordonnées x,y, l'adresse de la texture, et détermine l'adresse de la tile à lire.
[[File:Unité de texture simple.png|centre|vignette|upright=2|Unité de texture simple]]
===La gestion des accès mémoire===
Enfin, l'unité de texture doit tenir compte du fait que la mémoire vidéo met du temps à lire une texture. En théorie, l'unité de texture ne devrait pas accepter de nouvelle demande de lecture tant que celle en cours n'est pas terminée. Mais faire ainsi demanderait de bloquer tout le pipeline, de l'''input assembler'' au unités de''shaders'', ce qui est tout sauf pratique et nuirait grandement aux performances.
Une solution alternative consiste à mettre en attente les demandes de lectures de texture pendant que la mémoire est occupée. La manière la plus simple d'implémenter des accès mémoire multiples est de les mettre en attente dans une petite mémoire FIFO. Cela implique que les accès mémoire s’exécutent dans l'ordre demandé par le ''shader'' et/ou l'unité de rastérisation, il n'y a pas de réorganisation des accès mémoire ou d’exécution dans le désordre des accès mémoire.
[[File:Texture prefetching.png|centre|vignette|upright=1.5|Accès mémoire simultanés.]]
Évidemment, quand la mémoire FIFO est pleine, le pipeline est alors totalement bloqué. Le rasteriser est prévenu que l'unité de texture ne peut pas accepter de nouvelle lecture de texture. En pratique, la FIFO est généralement d'une taille respectable et permet de mettre en attente beaucoup de demandes de lecture de texture. Il faut de plus noter qu'il y a une FIFO par processeur de ''shader'' sur les cartes graphiques modernes. Quand elle est pleine, le processeur cesse d'exécuter de nouveaux accès mémoire, mais peut continuer à exécuter des ''shaders'' dans les autres unités de calcul, pas besoin de bloquer complétement le pipeline.
===L'intégration du cache de textures===
Il faut noter que les unités de texture incorporent aussi un cache de texture, voire plusieurs. L'intégration des caches de texture avec la mémoire FIFO précédente est quelque peu compliqué, car il faut garantir que les lectures de texture se fassent dans le bon ordre. On ne peut pas exécuter une lecture dans le cache alors que des lectures précédentes sont en attente de lecture en mémoire vidéo. Et cela pose un gros problème : une lecture dans le cache de texture prend quelques dizaines de cycles d'horloge, alors qu'une lecture en mémoire vidéo en prend facilement 400 à 800 cycles, parfois plus. Et cela fait que l'ordre des accès mémoire peut s'inverser.
Prenons par exemple un accès au cache précédé et suivi par deux accès en mémoire vidéo. Le premier démarre au cycle 1, et se termine au cycle numéro 400. L'accès au cache commence au cycle 2 et se termine 20 cycles après, au cycle numéro 22. En clair, la lecture dans le cache s'est terminée avant l'accès mémoire qui le précède. Les textures ne sont donc plus lues dans l'ordre. Et il faut trouver une solution pour éviter cela.
La solution est de retarder les lectures dans le cache tant que tous les accès précédents ne sont pas terminés. Mais pour retarder les lectures en question, il faut d'abord savoir si la lecture atterrit dans le cache ou non, ce qui demande d'accéder au cache. On fait face à un dilemme : on veut retarder les accès au cache, mais les différencier des lectures déclenchant des accès mémoire demande d'accéder au cache en premier lieu. La solution est décrite dans l'article "Prefetching in a Texture Cache Architecture" par Igehy et ses collègues. Elle se base sur deux idées combinées ensemble.
La première idée est de séparer l'accès au cache en deux : une étape qui vérifie si les texels à lire sont dans le cache, et une étape qui accède aux données dans le cache lui-même. Un cache de texture est donc composé de deux circuits principaux. Le premier vérifie la présence des texels dans le cache. Il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Ensuite, en plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
La seconde idée est de retarder l'accès au cache entre les deux phases. La première étape d'un accès mémoire vérifie si la donnée est dans le cache ou non. Puis, on retarde la lecture des données, pour attendre que toutes les lectures précédentes soient terminées. Et enfin, troisième étape : la lecture des texels dans la mémoire cache proprement dite. Les accès mémoire passant par la mémoire vidéo se font de la même manière, à une différence près : la lecture dans le cache est remplacée par la lecture en mémoire vidéo. Tout démarre avec une demande à l'unité de tags, qui vérifie si le texel est dans le cache ou non. Puis on retarde l'accès tant que la mémoire vidéo est occupée, puis on effectue la lecture en mémoire vidéo.
Si ce n'est pas le cas, l'accès mémoire est envoyé à la mémoire vidéo comme précédemment, à savoir qu'il est mis en attente dans une mémoire FIFO, puis envoyé à la mémoire vidéo dès que celle-ci est libre. Mais en sortie de la mémoire, la donnée lue est envoyée dans le cache de texture, par dans l'unité de filtrage. Pour savoir où placer la donnée lue, l'unité de tag a réservé une ligne de cache précise, une adresse bien précise. L'adresse en question est disponible en lisant une autre mémoire FIFO, qui a mis en attente l'adresse en question, en attendant que l'accès mémoire se termine. La donnée est alors écrite dans le cache, puis lue par l'unité de filtrage de textures.
Pour une lecture dans le cache, le déroulement est similaire, mais sans le passage par la mémoire. La lecture fait une demande à l'unité de tag, et celle-ci répond que la donnée est bien dans le cache. Elle place alors l'adresse à lire dans la file d'attente. Une fois que les accès mémoire précédents sont terminés, l'adresse sort de la file d'attente et est envoyée à la mémoire de données. La lecture s'effectue, les texels sont envoyés à l'unité de filtrage de textures. La seule différence avec un ''phased cache'' normal est l'insertion de l'adresse à lire dans une FIFO qui vise à mettre en attente
[[File:Unité de texture avec un cache de texture.png|centre|vignette|upright=2.0|Unité de texture avec un cache de texture]]
Pour résumer, l'implémentation précédente garantit une exécution des lectures dans leur ordre d'arrivée. Et pour cela, elle retarde les lectures dans le cache tant que les lectures en mémoire précédentes ne sont pas terminées. L'accès au cache est plus rapide que l'accès en mémoire vidéo, mais le retard ajouté pour garantir l'ordre des lectures fait que le temps d'accès est très long.
==Le mip-mapping==
Le '''mip-mapping''' a pour but de légèrement améliorer les graphismes des objets lointains, tout en rendant les calculs de texture plus rapides. Formellement, le ''mip-mapping'' est une technique de filtrage de texture, mais nous l'abordons maintenant car elle est surtout liée au calcul d'adresse. Les unités de texture ont des circuits de filtrage de texture séparés des circuits de ''mip-mapping'' et de calcul d'adresse, d'où le fait que nous en parlons séparément.
Le problème résolu par le ''mip-mapping'' est le rendu des textures lointaines. Si une texture est plaquée sur un objet lointain, une bonne partie des détails est invisible pour l'utilisateur. Un pixel de l'écran est associé à plusieurs texels. Idéalement, la carte graphiques devrait lire tous ces texels et en faire une sorte de moyenne pondérée, pour calculer la couleur finale du pixel. Mais dans les faits, ce serait très gourmand et compliqué à implémenter en hardware. Une solution serait de ne garder que quelque texels, mais cela a tendance à créer des artefacts visuels (les textures affichées ont tendance à pixeliser). Le ''mip-mapping'' permet de réduire ces deux problèmes en même temps en précalculant cette moyenne pondérée pour des distances prédéfinies.
L'idée est d'utiliser plusieurs exemplaires d'une même texture à des résolutions différentes, chaque exemplaire étant adapté à une certaine distance. Par exemple, une texture sera stocké avec un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre de 128 * 128 et ainsi de suite jusqu’à un dernier exemplaire de 32 * 32 pixel. Chaque exemplaire correspond à un '''niveau de détail''', aussi appelé ''Level Of Detail'' (abrévié en LOD). La résolution utilisée diminue d'autant plus que l'objet est situé loin de la caméra. Les objets proches seront rendus avec la texture 512*512, ceux plus lointains seront rendus avec la texture de résolution 256*256, les textures 128*128 seront utilisées encore plus loin, et ainsi de suite jusqu'aux objets les plus lointains qui sont rendus avec la texture la plus petite de 32*32.
[[File:MipMap Example STS101.jpg|centre|vignette|upright=2|Exemples de mip-maps.]]
Le ''mip-mapping'' améliore grandement la qualité d'image. L'image d'exemple ci-dessous le montre assez bien.
[[File:Mipmapping example.png|centre|vignette|upright=2|Exemple de mipmapping.]]
Pour faciliter les calculs d'adresse, les LOD d'une même texture sont stockées les uns après les autres en mémoire (dans un tableau, comme diraient les programmeurs). Ainsi, pas besoin de se souvenir de la position en mémoire de chaque LOD : l'adresse de la texture de base, et quelques astuces arithmétiques suffisent. Prenons le cas où la texture de base a une taille L. le premier exemplaire est à l'adresse 0, le second niveau de détail est à l'adresse L, le troisième à l'adresse L + L/4, le suivant à l'adresse L + L/4 + L/16, et ainsi de suite. Le calcul d'adresse demande juste connaître le niveau de détails souhaité et l'adresse de base de la texture. Le niveau de détail voulu est calculé par les pixel shaders, en fonction de la coordonnée de profondeur du pixel à traiter.
Évidemment, cette technique consomme de la mémoire vidéo, vu que chaque texture est dupliquée en plusieurs exemplaires, en plusieurs LOD. Dans le détail, la technique du mip-mapping prend au maximum 33% de mémoire en plus (sans compression). Cela vient du fait qu'en prenant une texture dexu fois plus petite, elle prend 4 fois moins de mémoire : 2 fois moins de pixels en largeur, et 2 fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des LODs, texture de base comprise, prendra X + (X/4) + (X/16) + (X/256) + … Un petit calcul de limite donne 4/3 * X, soit 33% de plus.
===L'implémentation du mip-mapping dans l'unité de texture===
Le ''mip-mapping'' est lui aussi pris en charge par l'unité de calcul d'adresse, car cette technique change l'adresse de base de la texture. La gestion du ''mip-mapping'' est cependant assez complexe. Il est possible de laisser le pixel shader calculer quel niveau de détail utiliser, en fonction de la coordonnée de profondeur z du pixel à afficher. La carte graphique détermine alors automatiquement quelle texture lire, quel niveau de détail, automatiquement. Elle détermine aussi la bonne résolution pour la texture, qui est égal à la résolution de la texture de base, divisée par le niveau de détail. Pour résumer, le niveau de détail est envoyé aux unités de texture, qui s'occupent de calculer l'adresse de base et la résolution adéquates. Quelques calculs arithmétiques simples, donc, qui s'implémentent facilement avec quelques circuits.
Mais une autre méthode laisse la carte graphique déterminer le niveau de détail par elle-même. Dans ce cas, cela demande, outre les deux coordonnées de texture, de calculer la dérivée de ces deux coordonnées dans le sens horizontal et vertical, ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Les quatre dérivées sont les suivantes :
: <math>\frac{du}{dx}</math>, <math>\frac{dv}{dx}</math>, <math>\frac{du}{dy}</math>, <math>\frac{dv}{dy}</math>
Un bon moyen pour obtenir les dérivées demande de regrouper les pixels par groupes de 4 et de faire la différence entre leurs coordonnées de texture respectives. On peut calculer les deux dérivées horizontales en comparant les deux pixels sur la même ligne, et les deux dérivées verticales en comparant les deux pixels sur la même colonne. Mais cela demande de rastériser les pixels par groupes de 4, par ''quads''. Et c'est ce qui est fait sur les cartes graphiques actuelles, qui rastérisent des groupes de 4 pixels à la fois.
[[File:Texture sampler unit with mipmapping.png|centre|vignette|upright=2.0|Unité de texture avec mipmapping.]]
Malheureusement, le calcul exact utilisé pour le choix de la mip-map dépend du GPU considéré et peu de chose est connu quant à ces algorithmes. Il est possible d'inférer le comportement à partir d'observations, mais guère plus. Pour ceux qui veulent en savoir plus, je conseille la lecture de cet article de blog :
* [https://pema.dev/2025/05/09/mipmaps-too-much-detail/ Mipmap selection in too much detail]
==Le filtrage de textures==
Plaquer des textures sans autre forme de procès ne suffit pas à garantir des graphismes d'une qualité époustouflante. La raison est que les sommets et les texels ne tombent pas tout pile sur un pixel de l'écran : le sommet associé au texel peut être un petit peu trop en haut, ou trop à gauche, etc. Une explication plus concrète fait intervenir les coordonnées de texture. Souvenez-vous que lorsque l'on traduit une coordonnée de texture u,v en coordonnées x,y, on obtient un résultat qui ne tombe pas forcément juste. Souvent, le résultat a une partie fractionnaire. Si celle-ci est non-nulle, cela signifie que le texel/sommet n'est pas situé exactement sur le pixel voulu et que celui-ci est situé à une certaine distance. Concrètement, le pixel tombe entre quatre texels, comme indiqué ci-dessous.
[[File:Filtrage texture.png|centre|vignette|upright=2.0|Position du pixel par rapport aux texels.]]
Pour résoudre ce problème, on doit utiliser différentes techniques d'interpolation, aussi appelées techniques de '''filtrage de texture''', qui visent à calculer la couleur du pixel final en fonction des texels qui l'entourent. Il existe de nombreux types de filtrage de textures, qu'il s'agisse du filtrage linéaire, bilinéaire, trilinéaire, anisotropique et bien d'autres.
Tous ont besoin d'avoir certaines informations qui sont généralement fournies par les circuits de calcul d'adresse. La première est clairement la partie fractionnaire des coordonnées x,y. La seconde est la dérivée de ces deux coordonnées dans le sens horizontal et vertical., ce qui fait quatre dérivées (deux dérivées horizontales, deux verticales). Toujours est-il que le filtrage de texture est une opération assez lourde, qui demande beaucoup de calculs arithmétiques. On pourrait en théorie le faire dans les pixels shaders, mais le cout en performance serait absolument insoutenable. Aussi, les cartes graphiques intègrent toutes un circuit dédié au filtrage de texture, le ''texture sampler''. Même les plus anciennes cartes graphiques incorporent une unité de filtrage de texture, ce qui nous montre à quel point cette opération est importante.
[[File:Texture unit.png|centre|vignette|upright=2.0|Unité de texture.]]
On peut configurer la carte graphique de manière à ce qu'elle fasse soit du filtrage bilinéaire, soit du filtrage trilinéaire, on peut configurer le niveau de filtrage anisotropique, etc. Cela peut se faire dans les options de la carte graphique, mais cela peut aussi être géré par l'application. La majorité des jeux vidéos permettent de régler cela dans les options. Ces réglages ne concernent pas la texture elle-même, mais plutôt la manière dont l'unité de texture doit fonctionner. Ces réglages sur l''''état de l'unité de texture''' sont mémorisés quelque part, soit dans l'unité de texture elle-même, soit fournies avec la ressource de texture elle-même, tout dépend de la carte graphique. Certaines cartes graphiques mémorisent ces réglages dans les unités de texture ou dans le processeur de commande, et tout changement demande alors de réinitialiser l'état des unités de texture, ce qui prend un peu de temps. D'autres placent ces réglages dans les ressources de texture elles-mêmes, ce qui rend les modifications de configuration plus rapides, mais demande plus de circuits. D'autres cartes graphiques mélangent les deux options, certains réglages étant globaux, d'autres transmis avec la texture. Bref, difficile de faire des généralités, tout dépend du matériel et le pilote de la carte graphique cache tout cela sous le tapis.
Maintenant que cela est dit, voyons quelles sont les différentes méthodes de filtrage de texture et comment la carte graphique fait pour les calculer.
===Le filtrage au plus proche===
La méthode de filtrage la plus simple consiste à colorier avec le texel le plus proche. Cela revient tout simplement à ne pas tenir compte de la partie fractionnaire des coordonnées x,y, ce qui est très simple à implémenter en matériel. C'est ce que l'on appelle le '''filtrage au plus proche''', aussi appelé ''nearest filtering''.
Autant être franc, le résultat est assez pixelisé et peu agréable à l’œil. Par contre, le résultat est très rapide à calculer, vu qu'il ne demande aucun calcul à proprement parler. Elle ne fait pas appel à la parti fractionnaire des coordonnées entières de texture, ni aux dérivées de ces coordonnées. On peut combiner cette technique avec le mip-mapping, ce qui donne un résultat bien meilleur, bien que loin d'être satisfaisant. Au passage, toutes les techniques de filtrage de texture peuvent se combiner avec du mip-mapping, certaines ne pouvant pas faire sans.
[[File:Interpolation-nearest.svg|centre|vignette|Filtrage de texture au plus proche.]]
===Le filtrage linéaire===
Le filtrage le plus simple est le '''filtrage linéaire'''. Il effectue une interpolation linéaire entre deux mip-maps, deux niveaux de détails. Pour comprendre l'idée, nous allons prendre une situation très simple, avec une texture carrée de 512 texels de côté. Le mip-mapping crée plusieurs textures : une de 256 texels de côté, une de 128 texels, une de 64, etc. Maintenant, la texture est sur un objet à une certaine distance de l'écran, vu de face. Le résultat est qu'elle correspond à l'écran à un carré de 300 pixels de côté (pas d'erreur : pixels, pas texels). Dans ce cas, la texture se trouve entre deux mip-maps : celle de 512 pixels de côté, celle de 256. Laquelle choisir ? Le filtrage au plus proche prend la texture de 512 pixels de côté. Le filtrage linéaire lui, fait autrement.
Vu que la texture est entre deux mip-maps, l'idée est de prendre le texel au plus proche dans chaque texture et de faire une sorte de moyenne appelée l'interpolation linéaire. L'interpolation par du principe que la couleur varie entre les deux texels en suivant une fonction affine, illustrée ci-dessous. Ce ne serait évidemment pas le cas dans le monde réel, mais on supposer cela donne une bonne approximation de ce à quoi ressemblerait une texture à plus haute résolution. On peut alors calculer la couleur du pixel par une simple moyenne pondérée par la distance. Le résultat est que les transitions entre deux niveaux de détails sont plus lisses, moins abruptes.
[[File:Lin interp -é.png|centre|vignette|upright=2.0|Interpolation linéaire.]]
===Le filtrage bilinéaire===
Le filtrage bilinéaire effectue une sorte de moyenne pondérée des quatre texels les plus proches du pixel à afficher. Pour cela, rappelez-vous ce qui a été dit plus haut : les coordonnées x,y d'un pixel ont une partie entière et une partie fractionnaire. Le filtrage au plus proche élimine les parties fractionnaires, ce qui donne une coordonnée x,y. Avec le filtrage bilinéaire, on prend les texels de coordonnées (x,y) ; (x+1,y) ; (x,y+1) ; (x+1,y+1), le pixel étant entre ces 4 texels.
Mais le filtrage ne fait pas qu'une simple moyenne, il prend en compte les parties fractionnaires pour faire la moyenne. En effet, le pixel n'est pas au milieu du carré de texel, il est quelque part mais est souvent plus proche d'un texel que des autres. Et il faut donc pondérer la moyenne par les distances aux 4 texels. Pour cela, la moyenne est calculée à partir d'interpolations linéaires. Avec 4 pixels, nous allons devoir calculer la couleur de deux points intermédiaires. La couleur de ces deux points se calcule par interpolation linéaire, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.
[[File:Bilin3.png|centre|vignette|upright=2|Filtrage bilinéaire de texture.]]
Le circuit qui permet de faire l'interpolation bilinéaire est particulièrement simple. On trouve un circuit de chaque pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu, et un pour la transparence. Chacun de ces circuit est composé de sous-circuits chargés d'effectuer une interpolation linéaire, reliés comme suit.
[[File:Texture sampler unit.png|centre|vignette|Unité de filtrage bilinéaire.]]
Vous noterez que le filtrage bilinéaire accède à 4 pixels en même temps. Fort heureusement, les textures sont stockées de manière à ce qu'on puisse charger les 4 pixels en une fois, comme on l'a vu plus haut. Le filtrage bilinéaire a de fortes chances que les 4 pixels filtrés soient dans la même ''tile'', la seule exception étant quand ils sont tout juste sur le bord d'une ''tile''.
: La console de jeu Nintendo 64 n'utilise que trois pixels au lieu de quatre dans son interpolation bilinéaire, qui en devient une interpolation quasi-bilinéaire. La raison derrière ce choix est une question de performances, comme beaucoup de décisions de ce genre. Le résultat est un rendu imparfait de certaines textures.
===Le filtrage trilinéaire===
Avec le filtrage bilinéaire, des discontinuités apparaissent sur certaines surfaces. Par exemple, pensez à une texture de sol : elle est appliquée plusieurs fois sur toute la surface du sol. A une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512*512 à 256*256, ce qui est visible pour un joueur attentif. De telles transitions sont lissées grâce au filtrage linéaire, il n'y a plus qu'à le combiner avec le filtrage bilinéaire. Rien d’incompatible : le premier filtre l'intérieur d'une mip-map, le second combine deux mip-maps.
Le filtrage trilinéaire prend les deux mip-maps les plus proches, fait un filtrage bilinéaire avec chacune, puis fait une « une moyenne » pondérée entre les deux résultats. Le circuit de filtrage trilinéaire existe en plusieurs versions. La plus simple, illustrée ci-dessous, effectue deux filtrages bilinéaires en parallèle, dans deux circuits séparés, puis combine leurs résultats avec un circuit d'interpolation linéaire. Mais ce circuit nécessite de charger 8 texels simultanément. Qui plus est, ces 8 texels ne sont pas consécutifs en mémoire, car ils sont dans deux niveaux de détails/mip-maps différents.
[[File:Parallel trilinear filtering.png|centre|vignette|upright=2.0|Unité de filtrage trilinéaire parallèle.]]
Vu qu'on lit des texels dans deux mip-maps, les texels sont lus en deux fois : 4 texels provenant de la première mip-map, suivis par les 4 texels de l'autre mip-map. Les 4 premiers texels doivent donc être mis en attente dans des registres, en attendant que les 4 autres arrivent. Une amélioration du circuit précédent gère cela en ajoutant des registres. Il lit les 4 premiers texels, les filtre avec une interpolation bilinéaire, et mémorise le résultat dans un registre. Puis, il lit les 4 autres texels, les filtre, et met le résultat dans un second registre. A ce moment là, un circuit d'interpolation linéaire finit le travail. On économise donc un circuit d'interpolation bilinéaire, sans que les performances soient trop impactées.
[[File:Filtrage trilineaire.png|centre|vignette|upright=1.0|Unité de filtrage trilineaire série.]]
Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le temps où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. À chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients, et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.
===Le filtrage anisotrope===
D'autres artefacts peuvent survenir lors de l'application d'une texture, la perspective pouvant déformer les textures et entraîner l'apparition de flou. La raison à cela est que les techniques de filtrage de texture précédentes partent du principe que la texture est vue de face. Prenez une texture carrée, par exemple. Vue de face, elle ressemble à un carré sur l'écran. Mais tournez la caméra, de manière à voir la texture de biais, avec un angle, et vous verrez que la forme de la texture sur l'écran est un trapèze, pas un carré. Cette déformation liée à la perspective n'est pas prise en compte par les méthodes de filtrage de texture précédentes. Pour le dire autrement, les techniques de filtrage précédentes partent du principe que les 4 texels qui entourent un pixel forment un carré, ce qui est vrai si la texture est vue de face, sans angle, mais ne l'est pas si la texture n'est pas perpendiculaire à l'axe de la caméra. Du point de vue de la caméra, les 4 texels forment un trapèze d'autant moins proche d'un carré que l'angle est grand.
Pour corriger cela, les chercheurs ont inventé le '''filtrage anisotrope'''. En fait, je devrais plutôt dire : LES filtrages anisotropes. Il en existe un grand nombre, dont certains ne sont pas utilisés dans les cartes graphiques actuelles, soit car ils trop gourmand en accès mémoires et en calculs pour être efficaces, soit car ils ne sont pas pratiques à mettre en œuvre. Il est très difficile de savoir quelles sont les techniques de filtrage de texture utilisées par les cartes graphiques, qu'elles soient récentes ou anciennes. Beaucoup de ces technologies sont brevetées ou gardées secrètes, et il faudrait vraiment creuser les brevets déposés par les fabricants de GPU pour en savoir plus. Les algorithmes en question seraient de plus difficiles à comprendre, les méthodes mathématiques cachées derrière ces méthodes de filtrage n'étant pas des plus simple.
[[File:Anisotropic filtering en.png|centre|vignette|upright=2|Exemple de filtrage anisotrope.]]
==La compression de textures==
Les textures les plus grosses peuvent aller jusqu'au mébioctet, ce qui est beaucoup. Pour limiter la casse, les textures sont compressées. La '''compression de texture''' réduit la taille des textures, ce qui peut se faire avec ou sans perte de qualité. Elle entraîne souvent une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande. Mais il s'agit là d'une technique très simple, beaucoup plus simple que les techniques que nous allons voir dans cette section. Nous allons voir quelque algorithmes de compression de textures de complexité intermédiaire, mais n'allons pas voir l'état de l'art. Il existe des formats de texture plus récents que ceux qui nous allons aborder, comme l{{'}}''Ericsson Texture Compression'' ou l{{'}}''Adaptive Scalable Texture Compression'', plus complexes et plus efficaces.
Notons que les textures sont compressées dans les fichiers du jeu, mais aussi en mémoire vidéo. Les textures sont décompressées lors de la lecture. Pour cela, la carte graphique contient alors un circuit, capable de décompresser les textures lorsqu'on les lit en mémoire vidéo. Les cartes graphiques supportent un grand nombre de formats de textures, au niveau du circuit de décompression. Du fait que les textures sont décompressées à la volée, les techniques de compression utilisées sont assez particulières. La carte graphique ne peut pas décompresser une texture entière avant de pouvoir l'utiliser dans un ''pixel shader''. A la place, on doit pouvoir lire un morceau de texture, et le décompresser à la volée. On ne peut utiliser les méthodes de compression du JPEG, ou d'autres formats de compression d'image. Ces dernières ne permettent pas de décompresser une image morceau par morceau.
Pour permettre une décompression/compression à la volée, les textures sont des textures tilées, généralement découpées en tiles de 4 * 4 texels. Les ''tiles'' sont compressées indépendamment les unes des autres. Et surtout, avec ou sans compression, la position des tiles en mémoire ne change pas. On trouve toujours une tile tous les T octets, peu importe que la tile soit compressée ou non. Par contre, une tile compressée n'occupera pas T octets, mais moins, là où une tile compressée occupera la totalité des T octets. En clair, compresser une tile fait qu'il y a des vides entre deux tiles dans al mémoire vidéo, mais ne change rien à leur place en mémoire vidéo qui est prédéterminée, peu importe que la texture soit compressée ou non. L'intérêt de la compression de textures n'est pas de réduire la taille de la texture en mémoire vidéo, mais de réduire la quantité de données à lire/écrire en mémoire vidéo. Au lieu de lire T octets pour une tile non-compressée, on pourra en lire moins.
===La palette indicée et la technique de ''Vector quantization''===
La technique de compression des textures la plus simple est celle de la '''palette indicée''', que l'on a entraperçue dans le chapitre sur les cartes d'affichage. La technique de '''''vector quantization''''' peut être vue comme une amélioration de la palette, qui travaille non pas sur des texels, mais sur des ''tiles''. À l'intérieur de la carte graphique, on trouve une table qui stocke toutes les ''tiles'' possibles. Chaque ''tile'' se voit attribuer un numéro, et la texture sera composé d'une suite de ces numéros. Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l’embarqué utilisent ce genre de compression.
===Les algorithmes de ''Block Truncation coding''===
La première technique de compression élaborée est celle du '''''Block Truncation Coding''''', qui ne marche que pour les images en niveaux de gris. Le BTC ne mémorise que deux niveaux de gris par ''tile'', que nous appellerons couleur 1 et couleur 2, les deux niveaux de gris n'étant pas le même d'une ''tile'' à l'autre. Chaque pixel d'une ''tile'' est obligatoirement colorié avec un de ces niveaux de gris. Pour chaque pixel d'une ''tile'', on mémorise sa couleur avec un bit : 0 pour couleur 1, et 1 pour couleur 2. Chaque ''tile'' est donc codée par deux entiers, qui codent chacun un niveau de gris, et une suite de bits pour les pixels proprement dit. Le circuit de décompression est alors vraiment très simple, comme illustré ci-dessous.
[[File:Block Truncation coding.jpg|centre|vignette|upright=2.0|Block Truncation coding.]]
La technique du BTC peut être appliquée non pas du des niveaux de gris, mais pour chaque composante Rouge, Vert et Bleu. Dans ces conditions, chaque ''tile'' est séparée en trois sous-''tiles'' : un sous-bloc pour la composante verte, un autre pour le rouge, et un dernier pour le bleu. Cela prend donc trois fois plus de place en mémoire que le BTC pur, mais cela permet de gérer les images couleur.
===Le format de compression S3TC / DXTC===
L'algorithme de '''Color Cell Compression''', ou CCC, améliore le BTC pour qu'il gère des couleurs autre que des niveaux de gris. Ce CCC remplace les deux niveaux de gris par deux couleurs. Une ''tile'' est donc codée avec un entier 32 bits par couleur, et une suite de bits pour les pixels. Le circuit de décompression est identique à celui utilisé pour le BTC.
[[File:Color Cell Compression.jpg|centre|vignette|Color Cell Compression.]]
[[File:Dxt1-memory-layout.png|vignette|Dxt1 et ''color cell compression''.]]
Le format de compression de texture utilisé de base par Direct X, le DXTC, est une version amliorée de l'algorithme précédent. Il est décliné en plusieurs versions : DXTC1, DXTC2, etc. La première version du DXTC est une sorte d'amélioration du CCC : il ajoute une gestion minimale de transparence, et découpe la texture à compresser en ''tiles'' de 4 pixels de côté. La différence, c'est que la couleur finale d'un texel est un mélange des deux couleurs attribuée au bloc. Pour indiquer comment faire ce mélange, on trouve deux bits de contrôle par texel.
Si jamais la couleur 1 < couleur2, ces deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (2 * Couleur1 + Couleur2) / 3
* 11 = (Couleur1 + 2 * Couleur2) / 3
Sinon, les deux bits sont à interpréter comme suit :
* 00 = Couleur1
* 01 = Couleur2
* 10 = (Couleur1 + Couleur2) / 2
* 11 = Transparent
[[File:DXTC.jpg|centre|vignette|DXTC.]]
Le circuit de décompression du DXTC ressemble alors à ceci :
[[File:Circuit de décompression du DXTC.jpg|centre|vignette|upright=2.0|Circuit de décompression du DXTC.]]
===Les format DXTC 2, 3, 4 et 5 : l'ajout de la transparence===
Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3, lui-même replacé par le DXT4 et par le DXT5. Dans le DXT3, la transparence fait son apparition. Pour cela, on ajoute 64 bits par ''tile'' pour stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1.
[[File:Dxt23-memory-layout.png|centre|vignette|Dxt 2 et 3.]]
Dans le DXT4 et le DXT5, la méthode utilisée pour compresser les couleurs l'est aussi pour les valeurs de transparence. L'information de transparence est stockée par un en-tête contenant deux valeurs de transparence, le tout suivi d'une matrice qui attribue trois bits à chaque texel. En fonction de la valeur des trois bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1.
[[File:Dxt45-memory-layout.png|centre|vignette|Dxt 4 et 5.]]
===Le format de compression PVRTC===
Passons maintenant à un format de compression de texture un peu moins connu, mais pourtant omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle travaille surtout dans les cartes graphiques embarquées. Ses cartes se trouvent notamment dans l'ipad, l'iPhone, et bien d'autres smartphones actuels.
Avec le PVRTC, les textures sont encore une fois découpées en ''tiles'' de 4 texels par 4, mais la ressemblance avec le DXTC s’arrête là. Chacque ''tile'' est codée avec :
* une couleur codée sur 16 bits ;
* une couleur codée sur 15 bits ;
* 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
* et un bit de modulation, qui permet de configurer l’interprétation des bits de mélange.
Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4*4.
==Annexe : le ''normal-mapping'' hardware==
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, parlons un peu du ''normal mapping'' et de son implémentation dans les unités de texture. Pour rappel, 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. Et pour comprendre quel est le rapport avec les textures, nous allons devoir faire quelques rappels sur l'éclairage par pixel.
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
L'éclairage se fait en utilisant de nombreux calculs, qu'on a détaillé dans le chapitre sur les bases du rendu 3D. Ceux-ci utilisent la normale d'un sommet, à savoir un vecteur orienté à la verticale de la surface. En théorie, il y a une normale par sommet, ce qui fait que les calculs d'éclairage doivent se faire au niveau géométrique, avant la rastérisation. Mais pour obtenir un éclairage de meilleure qualité, il y a des techniques qui calcule l'éclairage d'une scène 3D pixel par pixel.
Une première technique d'éclairage par pixel interpole les normales lors de l'étape de rastérisation. On obtient alors un éclairage de Phong. Une autre solution est celle du ''normal mapping''. Le '''''normal mapping''''' précalcule les normales d'une surface dans une texture, appelée la ''normal map''. L'éclairage est alors réalisé par un pixel shader, qui lit les normales depuis cette texture et fait les calculs d'éclairage avec.
===L'usage de textures non-compressées pour les ''normal maps''===
La ''normal map'' est une texture, donc. Mais ce n'est pas une texture comme une autre. Elle mémorise un vecteur pour chaque texel, pas une couleur. Il est possible d'utiliser les formats de textures non-compressés, même si cela ne donne pas de bons résultats. Après tout, un vecteur est codé par trois coordonnées x,y,z, il est possible de coder chacune avec un octet et de packager cela dans une texture RGB classique. La coordonnée x va dans la composante Rouge, la coordonnée Y dans la composante Bleu, et la coordonnée z dans la composante z.
Une première optimisation est de ne pas mémoriser les trois coordonnées, mais seulement deux d'entre elles et de calculer la troisième. En effet, les normales sont des vecteurs ''normalisés'', c'est à dire que leur longueur vaut 1, par construction. Vu que la taille est connue à l'avance, on peut en déduire la coordonnée z à partir des coordonnées x et y, avec l'usage du théorème de Pythagore. Par définition, <math>z^2 = 1 - x^2 - y^2</math>. Le calcul peut être fait dans le ''pixel shader'' sans problème.
: Pour être précis, il faut que les normales soient définies d'une certaine manière pour que ça marche. Les normales ne sont pas définies dans le même système de coordonnées que le modèle 3D, mais dans un autre système appelé l'''espace tangent''.
Notons que l'accès à la ''normal map'' se fait comme pour n'importe quelle texture : les texels sont lus en mémoire vidéo, puis le filtrage de texture est appliqué, et enfin le tout est envoyé au ''pixel shader''. En théorie, le filtrage effectue une sorte d'interpolation des normales automatique, ce qui permet de trouver la normale à un pixel précis, même s'il n'est pas sur un texel.
Les solutions précédentes demandent d'utiliser des textures non-compressées, qui utilisent beaucoup de mémoire vidéo. Utiliser des textures compressées pourrait sembler résoudre le problème. Mais l'usage de textures compressées marche très mal pour les ''normal maps'', elles ne permettent pas d'obtenir une qualité ou une compression suffisante. Aussi, des formats de texture dédiés aux ''normal maps'' ont été inventés.
===Le format de texture 3Dc===
Le problème est que le rendu final n'est pas très beau. Et les opérations de filtrage ne donnent pas de très bons résultats. Pour remédier à ces problèmes, des formats de texture spécialisés pour les ''normal map'' ont été inventés. Le premier d'entre eux est le '''3Dc''', aussi appelé BC5 pour ''Block Compression 5''. Il utilise un octet par texel, au lieu de trois avec une texture RGB normale non-compressée. Et surtout : il est géré en matériel, directement dans l'unité de texture, qui peut décompresser les textures 3Dc et les filtrer !
Avec le 3Dc, seules coordonnées sont mémorisées, la troisième est calculée, comme dit plus haut. Pour le reste, la compression de la ''normal map'' ressemble un peu à celle des textures. La ''normal map'' est découpée en blocs de 4 par 4 texels de côté, chacune étant encodée en tenant compte de certaines redondances. Dans un bloc, les coordonnées x sont dans un certain intervalle, allant de <math>x_{min}</math> à <math>x_{max}</math>, idem pour la coordonnée y qui est dans l'intervalle <math>y_{min}</math>, <math>y_{max}</math>. L'intervalle <math>x_{min}</math> à <math>x_{max}</math> est coupé en 8 parts égales, ce qui fait 8 valeurs possibles dans cet intervalle, chacune correspondant à une valeur x possible pour une normale du bloc. La normale est donc encodée en précisant quelle valeur est la bonne, en utilisant un indice de 3 bits pour cela. La valeur exacte de la coordonnée x se calcule à partir de l'indice comme suit :
: <math>x = x_{min} + \left( \frac{x_{min} - x_{min}}{8} \right) \times \text{indice} </math>
Pour encoder un bloc de 4 par 4 normales, il faut donc :
* Un octet pour chaque coordonnée <math>x_{min}</math>, <math>x_{max}</math>, <math>y_{min}</math>, <math>y_{max}</math>.
* 6 bits par normale : 3 pour encoder le décalage de la coordonnée x, 3 pour la coordonnée y.
Le tout permet d'encoder un bloc de 16 normales en seulement 128 bits, soit un octet par normale. Le ''pixel shader'' se débrouille pour décompresser un bloc. Il effectue notamment le calcul du dessus, pour retrouver la coordonnée x. Les calculs étant particulièrement simples, le cout en performance est très faible. Et ce d'autant plus qu'ils demandent juste de faire des additions et des multiplications entières, que l'unité scalaire peut faire en parallèle d'autres opérations plus gourmandes. Et le cout en calculs est de toute façon compensé par l'économie de mémoire et de bande passante lié à la compression : diviser la taille des données par trois, ca a un sacré impact !
==Annexe : les ''shadowmap'' hardware==
Les anciens GPU, notamment la Geforce FX, avaient des fonctionnalités spécifiques pour le calcul des ombres. Dans la plupart des jeux vidéos de l'époque, et même de maintenant, les ombres sont calculées avec la technique des ''shadowmap''. L'idée est assez simple sur le principe : un pixel est dans l'ombre quand il est invisible depuis une source de lumière.
L'idée est que le rendu est réalisé en plusieurs passes, avec une passe par source de lumière et une passe finale pour calculer l'image finale. Nous allons expliquer la technique avec une seule source de lumière, et allons utiliser l'exemple de la scène ci-dessous.
[[File:7fin.png|centre|vignette|Scène 3D d'exemple.]]
===La technique du ''shadowmapping''===
[[File:2shadowmap.png|vignette|Résultat de la première passe : ''shadowmap''..]]
La première passe rend l'image depuis le point de vue de la source de lumière. Cette première passe ne rend pas les couleurs de la scène, elle ne s'intéresse qu'à la profondeur des pixels. Le résultat est que l'image ne rend que le tampon de profondeur. Celui-ci est ensuite réutilisé comme texture pour la passe suivante. La texture en question est appelée la '''''shadownmap'''''.
La perspective utilisée, ainsi que le ''view frustrum'', dépend de la source de lumière. Pour une source de lumière qui émet un cône de lumière, le ''view frustrum'' de l'image rendue doit contenir tout le cone de lumière, et doit coller le plus possible à celui-ci. Pour une source directionnelle, comme le soleil, une perspective orthographique est utilisée.
La seconde passe rend l'image du point de vue de la caméra, pour rendre l'image finale. Elle rend l'image finale, qui est composée de pixels, chacun ayant une position à l'écran x,y, et une profondeur z. Les coordonnées sont transformées pour obtenir la position de ce pixel depuis le point de vue de la caméra. Une simple multiplication de matrice suffit, rien de bien compliqué, un shader peut le faire.
[[File:5failed.png|vignette|Résultat du test des comparaisons.]]
Après cette étape, on a alors les coordonnées x,y,z de ce pixel du point de vue de la caméra, et la ''shadowmap''. Il est alors possible d'accéder à la ''shadowmap'' au même endroit, à la même place que le pixel testé, aux mêmes coordonnées x,y. Si la profondeur du pixel est supérieure à celle de la shadowmap au même endroit, alors le pixel est situé derrière la surface visible, donc est dans l'ombre. Sinon, il n'est pas dans l'ombre. Le même procédé est répété sur chaque pixel de l'écran.
===Les optimisations hardware du ''shadowmapping''===
La technique des ''shadowmap'' demande donc de calculer une texture ''shadowmap'', puis de lire celle-ci et de faire des comparaisons de profondeur. Les GPU comme la Geforce FX intégraient du matériel dans les unités de texture pour faciliter ce travail. Les unités de texture pouvaient lire les ''shadowmap'', et faire la comparaison de profondeur toutes seules, elles avaient des circuits pour. Il suffisait de leur fournir le pixel à tester, ses coordonnées x,y,z, et l'adresse de la ''shadowmap''. Les unités de texture renvoyaient alors un résultat valant 0 ou 1 : 1 si le pixel est dans l'ombre, 0 sinon.
Elles pouvaient même effectuer du filtrage de texture sur les ''shadowmap''. Mais le filtrage était différent de celui utilisé sur les autres textures : moyenner des valeurs de profondeur ne marche pas bien. Elles utilisaient des techniques de filtrage différentes : elles faisaient les tests de comparaison, puis faisaient la moyenne des résultats. Ainsi, pour du filtrage bilinéaire, elles lisaient 4 texels dans la ''shadowmap'', puis faisaient 4 tests de comparaison, et moyennaient les 4 résultats.
==Annexe : le cube-mapping==
[[File:Cube mapped reflection example 2.JPG|vignette|Exemple de reflets environnementaux.]]
L''''environnement-mapping''' simule les réflexions et autres effets graphiques sur une surface ou un objet 3D. L'idée est de plaquer une texture pré-calculée pour simuler l'effet de l'environnement sur un modèle 3D. Il en existe plusieurs versions différentes, mais la seule utilisée de nos jours est le ''cube-mapping'', où la texture de l'environnement est plaquée sur un cube, d'où son nom. Le cube en question est utilisé différemment suivant ce que l'on cherche à faire avec le ''cube-mapping''.
===Les utilisations du ''cubemapping''===
Les deux utilisations principales sont le rendu du ciel et des décors, et les réflexions sur la surface des objets. Dans les deux cas, l'idée est de précalculer ce que l'on voit du point de vue de la caméra. On place la caméra dans la scène 3D, on place un cube centré sur la caméra, le cube est texturé avec ce que l'on voit de l'environnement depuis la caméra/l'objet de son point de vue.
[[File:Panorama cube map.png|centre|vignette|upright=2|L'illustration montre en premier lieu une ''cubemap'' avec les six faces mises en évidence, puis quel environnement 3D elle permet de simuler, le troisième illustration montrant comment la ''cubemap'' est utilisée pour simuler l'environnement.]]
Le rendu du ciel et des décors lointains dans les jeux vidéo se base sur des '''''skybox''''', à savoir un cube centré sur la caméra, sur lequel on ajoute des textures de ciel ou de décors lointains. Le cube est recouvert par une texture, qui correspond à ce que l'on voit quand on dirige le regard de la caméra vers cette face. Contrairement à ce qu'on pourrait croire, la skybox n'est pas les limites de la scène 3D, les limites du niveau d'un jeu vidéo ou quoique ce soit d'autre de lié à la physique de la scène 3D. La skybox est centrée sur la caméra, elle suit la caméra dans son mouvement. Centrer la skybox sur la caméra permet de simuler des décors très lointains, suffisamment lointain pour qu'on n'ait pas l'illusion de s'en rapprocher en se déplaçant dans la map. De plus, cela évite d'avoir à faire trop de calculs à chaque fois que l'on bouge la caméra. La texture plaquée sur le cube est une texture unique, elle-même découpée en six sous-textures, une par face du cube.
[[File:Skybox example.png|centre|vignette|upright=2|Exemple de Skybox.]]
[[File:Cube mapped reflection example.jpg|vignette|Réflexions calculées par une ''cubemap''.]]
Le ''cube-mapping'' est aussi utilisé pour des reflets. L'idée est de simuler les reflets en plaquant une texture pré-calculée sur l'objet réflecteur. La texture pré-calculée est un dessin de l'environnement qui se reflète sur l'objet, un dessin du reflet à afficher. En la plaquant la texture sur l'objet, on simule ainsi des reflets de l'environnement, mais on ne peut pas calculer d'autres reflets comme les reflets objets mobiles comme les personnages. Et il se trouve que la texture pré-calculée est une ''cubemap''. Pour les environnements ouverts, c'est la ''skybox'' qui est utilisée, ce qui permet de simuler les reflets dans les flaques d'eau ou dans des lacs/océans/autres. Pour les environnements intérieurs, c'est une cubemap spécifique qui utilisée. Par exemple, pour l'intérieur d'une maison, on a une ''cubemap'' par pièce de la maison. Les reflets se calculent en précisant quelle ''cubemap'' appliquer sur l'objet en fonction de la direction du regard.
[[File:Cube map level.png|centre|vignette|Cube map de l'intérieur d'une pièce d'un niveau de jeux vidéo.]]
===L'implémentation matérielle du ''cubemapping''===
Toujours est-il que les textures utilisées pour le ''cubemapping'', appelées des ''cubemaps'', sont en réalité la concaténation de six textures différentes. En mémoire vidéo, la ''cubemap'' est stockée comme six textures les unes à la suite des autres. Lors du rendu, on doit préciser quelle face du cube utiliser, ce qui fait 6 possibilités. On a le même problème qu'avec les niveaux de détail, sauf que ce sont les faces d'une ''cubemap'' qui remplacent les textures de niveaux de détails. L'accès en mémoire doit donc préciser quelle portion de la ''cubemap'' il faut accéder. Et l'accès mémoire se complexifie donc. Surtout que l'accès en question varie beaucoup suivant l'API graphique utilisée, et donc suivant la carte graphique.
Le support des ''cubemaps'' dépend de l'API 3D, et surtout de si elle date ou si elle est récente :
* Les API 3D très anciennes ne gérent pas nativement les ''cubemaps'', qui doivent être émulées en logiciel en utilisant six textures différentes. Le pixel shader décide donc quelle ''cubemap'' utiliser, avec quelques calculs sur la direction du regard.
* Les API 3D récentes gèrent nativement les ''cubemaps''. Pour les versions les plus vielles de ces API, les six faces sont numérotées et l'accès à une ''cubemap'' précise quel face utiliser en donnant son numéro. La carte graphique choisit alors automatiquement la bonne texture. Mais cela demande de laisser le calcul de la bonne face au pixel shader.
* Dans les API 3D modenres, les ''cubemap'' sont gérées comme des textures en trois dimensions, adressées avec trois coordonnées u,v,w. La carte graphique utilise ces trois coordonnées de manière à en déduire quelle est la face pertinente, mais aussi les coordonnées u,v dans la texture de la face.
==Annexe : les textures virtuelles==
Les '''textures virtuelles''' sont une optimisation des textures normales, qui visent à accélérer le rendu de terrains de grande taille. Imaginez par exemple un monde assez ouvert, comme un environnement en forêt ou en montagne, avec une grande distance de visibilité. Avec de tels terrains, le "sol" est recouvert par une texture de sol unique qui recouvre tout le terrain. Elle ne se répète pas, est de très grande taille, et peut parfois recouvrir toute la map ! Mais il n'y a pas assez de mémoire vidéo pour mémoriser la texture toute entière. La seule solution est la suivante : une partie de la texture est placée en mémoire vidéo, le reste est soit placé en mémoire RAM ou sur le disque dur.
Pour cela, le moteur de jeu utilise une optimisation ingénieuse, basée sur une observation assez basique : une bonne partie de la texture est visible, mais le reste est caché par des arbres, des habitations ou d'autres obstacles. Une optimisation possible de ne garder en mémoire vidéo que les portions visibles de la texture, pas les portions cachées. Une autre optimisation mélange textures virtuelles et ''mip-mapping''. L'idée est que pour les portions lointaines d'une texture, la texture utilisée est une ''mip-map'' de basse résolution. L'idée est alors de ne charger que la ''mip-map'' adéquate, pas les autres niveaux de détail. En clair, la texture de base n'est pas chargée en mémoire vidéo, mais la ''mip-map'' basse résolution l'est.
===Une texture à deux niveaux===
L'implémentation des textures virtuelles découpe les méga-textures en ''tiles'', en morceaux rectangulaires de taille modeste. En clair, le terrain est découpé en morceau rectangulaires/carrés. Seules les tiles nécessaires sont chargées en mémoire vidéo, pas les autres. Par exemple, les ''tiles'' non-visibles ne sont pas placées en mémoire vidéo, seules les ''tiles'' visibles le sont. De même, il y a une ''tile'' par niveau de mip-map : seul la tile correspondant le niveau adéquat est en mémoire vidéo, les autres niveaux de détail ne sont pas chargés. On peut faire une analogie avec la mémoire virtuelle, où les données sont découpées en pages, qui sont chargées en mémoire RAM à la demande, suivant les besoins, les données pouvant être swappées sur le disque dur si elles sont peu utilisées. Sauf qu'ici, il s'agit de textures qui sont découpées en pages chargées à la demande en mémoire vidéo, depuis la RAM système.
Une texture virtuelle est en réalité un système à deux niveaux : une liste de ''tiles'' et les ''tiles'' elles-mêmes. La liste de ''tiles'' est appelée un '''atlas de texture''', c'est un peu l'équivalent de la ''tilemap'' pour le rendu 2D. Rendre une texture demande de calculer quelle ''tile'' contient le texel à afficher, consulter la ''tile'' en question, puis récupérer le texel adéquat dans cette ''tile''. La ''tile'' est donc une texture, mais la texture à charger est choisie parmi un ensemble, qui est ici l'atlas de texture.
===L'implémentation : logicielle versus matérielle===
Les textures virtuelles ont été utilisées pour la première fois par les jeux Rage 1 et 2 d'IdSoftware, et quelques jeux ultérieurs comme DOOM 2016. IdSoftware les appelait des '''''mega-textures'''''. L'optimisation permettait des gains en performance assez impressionnants. Le jeu Rage 1 utilisait une texture carrée unique de 128k pixels de côté pour rendre le terrain. En théorie, une telle texture devrait prendre 64 giga-octets, mais le jeu tournait correctement avec 512 méga-octets de RAM, poussivement avec seulement 256 méga-octets de RAM.
De nos jours, les textures virtuelles sont supportées par beaucoup de jeux vidéos, les moteurs les plus courants gèrent de telles textures de manière logicielles. Mais quelques GPU récents supportent les textures virtuelles. Sur les GPU récents, l'atlas de texture est géré nativement par le matériel. Le GPU choisit quelle ''tile'', quelle texture choisir pour rendre le texel adéquat. Pour cela, le GPU calcule quelle ''tile'' charger, consulte l'atlas de texture, et lit la texture de ''tile'' adéquate.
Mais l'implémentation sur les GPU récents a de nombreuses limitations. La limitation la plus importante est que la taille des textures virtuelles ne peut pas dépasser la taille d'une texture normale, soit 32768 pixels de côté pour une texture carrée environ sur les GPU de 2020. De plus, le chargement d'une ''tile'' est très lent. En clair, dès qu'on veut changer de niveau de mip-map pour une tile, ou dès qu'une tile devient visible, le chargement de la tile peut facilement prendre plusieurs centaines de millisecondes. Le filtrage de texture est très complexe avec des textures virtuelles, ce qui fait que le filtrage de texture virtuelle est souvent soumis à des limitations que les textures normales n'ont pas, notamment pour le filtrage anisotropique.
{{NavChapitre | book=Les cartes graphiques
| prev=Le rasterizeur
| prevText=Le rasterizeur
| next=Les Render Output Target
| nextText=Les Render Output Target
}}{{autocat}}
ireub8wduqbf24iywa57skx58i99f7b
Les cartes graphiques/Les processeurs de shaders
0
69558
763431
763381
2026-04-10T23:49:39Z
Mewtow
31375
/* Les registres des processeurs de shaders */
763431
wikitext
text/x-wiki
Les '''''shaders''''' sont des programmes informatiques exécutés par la carte graphique, et plus précisément par des processeurs de ''shaders''. Un point très important à comprendre est que chaque triangle ou pixel d'une scène 3D peut être traité indépendamment des autres. Le tout se résume comme suit :
: '''L’exécution d'un shader génère un grand nombre d'instances de ce shader, chacune traitant un paquet de pixels/sommets différent.'''
En conséquence, il est possible de traiter chaque instance d'un ''shader'' en parallèle des autres, en même temps, au lieu de traiter les instances l'une après l'autre.
La conséquence est que les cartes graphiques sont des architectures massivement parallèles, à savoir qu'elles sont capables d'effectuer un grand nombre de calculs indépendants en même temps. De plus, le parallélisme utilisé est du parallélisme de données, à savoir qu'on exécute le même programme sur des données différentes, chaque donnée étant traitée en parallèle des autres. Les cartes graphiques récentes incorporent toutes les techniques de parallélisme de donnée au niveau matériel, et nous allons toutes les détailler dans ce chapitre. S'il fallait résumer, elles ont plusieurs processeurs/cœurs, chaque cœur est capable d’exécuter des instructions SIMD (ils ne font que cela, à vrai dire), les cœurs sont fortement multithreadés, et j'en passe.
[[File:CPU and GPU.png|vignette|Comparaison du nombre de processeurs et de cœurs entre CPU et GPU.]]
Le premier point est qu'une carte graphique contient de nombreux processeurs, qui eux-mêmes contiennent plusieurs unités de calcul. Savoir combien de cœurs contient une carte graphique est cependant très compliqué, car la terminologie utilisée par les fabricants de carte graphique est particulièrement confuse. Il n'est pas rare que ceux-ci appellent cœurs ou processeurs, ce qui correspond en réalité à une unité de calcul d'un processeur normal, sans doute histoire de gonfler les chiffres. Et on peut généraliser à la majorité de la terminologie utilisée par les fabricants, que ce soit pour les termes ''warps processor'', ou autre, qui ne sont pas aisés à interpréter.
L'architecture d'une carte graphique récente est illustrée ci-dessous. Rien de bien déroutant pour qui a déjà étudié les architectures à parallélisme de données, mais quelques rappels ou explications ne peuvent pas faire de mal. Le premier point est la présence d'un grand nombre de processeurs/cœurs, les rectangles en bleu/rouge. Chacun d'entre eux contient un grand nombre de circuits de calculs, avec des circuits de calcul simples mais nombreux en rouge, et une unité pour les calculs complexes (trigonométriques, racines carrées, autres) en rouge. Le tout est relié à une hiérarchie mémoire indiquée en vert, comprenant des mémoires locales en complément de la mémoire vidéo principale. Le tout est alimenté par une unité de répartition, le '''''Thread Execution Control Unit''''' en jaune, qui répartit les différentes instances du ''shader'' sur les différents processeurs. Elle est aussi appelée le '''processeur de commandes''', comme nous le verrons dans quelques chapitres. Nous utiliserons le terme processeur de commande dans ce qui suit.
[[File:NVIDIA GPU Accelerator Block Diagram.png|centre|vignette|upright=2.5|Ce schéma illustre l'architecture d'un GPU en utilisant la terminologie NVIDIA. Comme on le voit, la carte graphique contient plusieurs cœurs de processeur distincts. Chacun d'entre eux contient plusieurs unités de calcul généralistes, appelées processeurs de threads, qui s'occupent de calculs simples (en bleu). D'autres calculs plus complexes sont pris en charge par une unité de calcul spécialisée (en rouge). Ces cœurs sont alimentés en instructions par le processeur de commandes, ici appelé ''Thread Execution Control Unit'', qui répartit les différents shaders sur chaque cœur. Enfin, on voit que chaque cœur a accès à une mémoire locale dédiée, en plus d'une mémoire vidéo partagée entre tous les cœurs.]]
Les portions bleu, jaune et verte du schéma précédent méritent chacune un chapitre séparé. La hiérarchie mémoire en vert fera l'objet d'un chapitre ultérieur. Quant au répartiteur en jaune, il sera détaillé en profondeur dans le prochain chapitre. Dans ce chapitre, nous allons voir comment fonctionnent les processeurs de ''shaders'', la partie bleue. Nous allons voir que ceux-ci ne sont pas très différents des processeurs que l'on trouve dans les ordinateurs normaux, du moins dans les grandes lignes. Ce sont des processeurs séquentiels, qui exécutent des instructions les unes après les autres. Ils ont des instructions machines, des modes d'adressage, un assembleur, des registres et tout ce qui fait qu'un processeur est un processeur. Néanmoins, il y a une différence de taille : ce sont des processeurs adaptés pour effectuer un grand nombre de calculs en parallèle.
==Les registres des processeurs de shaders==
Un processeur de shaders contient beaucoup de registres, sans quoi il ne pourrait pas faire son travail efficacement. Les plus intuitifs sont les '''registres généraux''', aussi appelés registres temporaires, qui servent à mémoriser n'importe quelle donnée utilisable par le processeur. Ils servent un peu à tout, le processeur peut les manipuler à loisir. Tout processeur digne de ce nom en possède. Mais un processeur de ''shader'' dispose aussi de registres spécialisés, qu'on ne trouve que sur les processeurs de ''shaders'', qui servent à l'interfacer avec le reste du pipeline graphique.
[[File:Architecture carte graphique vertex avec texture.PNG|centre|vignette|upright=2|Architecture carte graphique vertex avec texture]]
===Les registres d'interface avec le pipeline graphique===
Les processeurs de ''vertex shader'' reçoivent des 'sommets provenant de l'''input assembler'' et envoient leur résultat au rastériseur. Les processeurs de ''pixel shader'' reçoit des données provenant de l'unité de rastérisation; et envoie son résultat final aux ROPs. Et pour cela, le processeur de shader a des registres dédiés, qui servent d'interface avec le reste du pipeline graphique.
Les '''registres d'entrée''' réceptionnent soit les sommets provenant de l'''input assembler'', soit les pixels provenant de l'unité de rastérisation. Les registres d'entrée sont en lecture seule, du point de vue du processeur de shader, seule l'unité de rastérisation peut écrire dedans. Ils sont initialisés avant l'exécution de l'instance du ''shader''.
Les '''registres de sortie''' sont là où le processeur stocke les résultats à envoyer, soit au rastériseur pour un ''vertex shader'', soit aux ROP pour un ''pixel shader''. Les registres de sorties sont en écriture seule. Pour donner un exemple, les ''vertex shaders'' ont au minimum un registre pour la position du sommet dans l'espace (trois coordonnées), un autre pour la couleur/luminosité du sommet, un autre pour la couleur du brouillard, un autre pour les coordonnées de texture.
{|class="wikitable"
|+ Registres de sortie des ''pixel/vertex shaders''
|-
! Vertex shader
! Pixel shader
|-
| Couleur du pixel
| Couleur du sommet
|-
| Profondeur du pixel
| Position du sommet
|-
| rowspan="2" |
| Coordonnées de texture du sommet
|-
| Couleur de brouillard.
|}
Il y a aussi des '''registres de texture''' , qui servent d'interface avec la mémoire pour la gestion des textures. Ils mémorisent les texels lus par l'unité de texture. L'unité de texture lit un texel, plusieurs avec ''multitexturing'', et les place dans ces registres de texture. Les registres de texture sont parfois initialisés avant l'exécution du ''shader'', mais la plupart sont initialisé quand le ''shader'' termine une instruction de lecture de texture. Ils sont généralement en lecture seule, mais il y a des exceptions.
===Les registres spécialisés internes===
D'autres registres spécialisés ne font pas l'interface avec le reste du GPU. Ils servent à stocker des constantes ou des données importantes, qui n'ont pas vraiment leur place dans les registres généraux.
Les '''registres de constantes''' servent pour stocker des constantes utiles pour le ''shader''. Par exemple, pour les ''vertex shaders'', ils stockent les matrices servant aux différentes étapes de transformation ou d'éclairage. Ces constantes sont placées dans ces registres peu après le chargement du vertex shader dans la mémoire vidéo. Toutefois, le vertex shader peut écrire dans ces registres, au prix d'une perte de performance particulièrement violente.
Les ''pixel/vertex shaders'' 1.0 ne géraient que des constantes flottantes pour les ''vertex shaders'', entières pour les ''pixel shaders''. Mais les ''pixel/vertex shaders'' 2.0 et 3.0 avaient des registres de constantes séparés pour les nombres entiers, les nombres flottants, et même les nombres booléens. Les constantes entières et booléennes étaient utilisées pour gérer les boucles, guère plus. Aussi, il y en avait 16, comparé aux centaines de registres de constantes flottants. Mais avec les ''pixel/vertex shaders'' 4.0 et plus, les registres de constante ont été fusionnés et n'ont plus de type prédéterminé, le programmeur gère ces registres comme il l'entend.
L'adressage des registres de constante est quelque peu particulier. Il faut dire qu'il y en a plusieurs milliers sur les processeurs de ''shaders'' modernes, au point qu'il serait plus juste de parler de mémoire RAM des constantes. Les registres de constante sont en effet un ''local store'' un peu spécial, intégré directement dans le processeur. Et le processeur accède à ce ''local store'' en utilisant une mode d'adressage semblable à celui utilisé pour la mémoire, avec un mode d'adressage indirect. L'adresse à lire dans ce ''local store'' est dans un registre, séparé du reste, appelé le '''registre d'adresse de constante'''.
Depuis les ''pixel/vertex shaders'' 3.0, les ''shaders'' sont capables d'effectuer des boucles et d'autres structures de contrôle familières pour les programmeurs. Et deux registres ont été intégrés afin d'améliorer les performances des structures de contrôle. Le premier est un registre à prédicat, qui sera vu dans la section sur le SIMD avec prédication. Le second est un '''registre compteur de boucle''', qui mémorise l'indice d'une boucle. Il est initialisé à 0, et est incrémenté à chaque fois qu'une boucle s'exécute.
==Les processeurs de shaders modernes : les processeurs SIMD==
Maintenant, voyons quelles sont les instructions supportées par les processeurs de shaders modernes. Et si je dis moderne, c'est car nous ne parlerons que des GPU de l'époque DirectX 10 et après, pas des GPU de l'époque DirectX 9 et antérieur. La raison est que le jeu d'instruction des shaders a franchement évolué, avec le passage d'architectures VLIW à des architectures SIMD. Et cela a eu des conséquences assez profondes sur le jeu d'instruction et leur microarchitecture. Nous n'allons parler des GPU de type SIMD dans ce chapitre. Un chapitre dédié sera consacré aux GPU de type VLIW.
Le jeu d'instruction des GPU NVIDIA n'est pas encore connu à l'heure où j'écris ces lignes, la documentation du constructeur n'est pas disponible. Quelques chercheurs ont tenté de faire de la rétro-ingénierie du code de divers shaders pour retrouver le jeu d'instruction des divers GPU NVIDIA, ce qui fait qu'on a cependant une idée de ce dernier. Mais rien d'officiel. Par contre, AMD fournit librement cette documentation sur le net. Ce qui fait qu'on peut trouver des documents de ce genre :
* [https://developer.amd.com/wordpress/media/2012/12/AMD_Southern_Islands_Instruction_Set_Architecture.pdf Graphics Core Next 1 instruction set] ;
* [https://developer.amd.com/wordpress/media/2013/07/AMD_Sea_Islands_Instruction_Set_Architecture.pdf Graphics Core Next 2 instruction set] ;
* [https://developer.amd.com/wordpress/media/2013/12/AMD_GCN3_Instruction_Set_Architecture_rev1.1.pdf Graphics Core Next 3 and 4 instruction sets] ;
* [https://developer.amd.com/wp-content/resources/Vega_Shader_ISA_28July2017.pdf Graphics Core Next 5 instruction set] ;
* [https://developer.amd.com/wp-content/resources/Vega_7nm_Shader_ISA.pdf "Vega" 7nm instruction set architecture] (also referred to as Graphics Core Next 5.1) ;
* [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna3-shader-instruction-set-architecture-feb-2023_0.pdf Jeu d'instruction des GPU de type RDNA3 d'AMD].
===Les instructions SIMD===
Les '''instructions SIMD''' manipulent plusieurs nombres en même temps. Elles manipulent plus précisément des '''vecteurs''', des ensembles de plusieurs nombres entiers ou nombres flottants placés les uns à côté des autres, le tout ayant une taille fixe, qui sont stockés dans des registres spécialisés. En général, tous les vecteurs ont une taille fixe, peu importe leur contenu. Cela implique que suivant la taille des données à manipuler, on pourra en placer plus ou moins dans un vecteur. Par exemple, un vecteur de 128 bits pourra contenir 4 entiers de 32 bits, 4 flottants 32 bits, ou 8 entiers de 16 bits.
[[File:Vector register.png|centre|vignette|upright=2|Contenu d'un vecteur en fonction du type de données utilisé.]]
Les vecteurs sont stockés dans des '''registres vectoriels''', aussi appelés '''registres SIMD'''. Un registre vectoriel peut contenir un vecteur complet, pas plus. En conséquence, ils ont une taille assez importante : ils font généralement 128, 256, voire 512 bits, comparé aux 32/64 bits des registres des CPU. Les cartes graphiques modernes contiennent un très grand nombre de registres SIMD.
{|
|+ Comparaison entre un processeur sans registres vectoriels, et avec registres vectoriels.
|[[File:Non-SIMD cpu diagram1.svg|vignette|upright=1.5|CPU Non-SIMD]]
|[[File:SIMD cpu diagram1.svg|vignette|upright=1.5|CPU SIMD]]
|}
Une instruction SIMD traite chaque donnée du vecteur indépendamment des autres. Par exemple, une instruction d'addition vectorielle va additionner ensemble les données qui sont à la même place dans deux vecteurs, et placer le résultat dans un autre vecteur, à la même place.
[[File:Instructions SIMD.png|centre|vignette|upright=2.0|Instructions SIMD]]
Sur les cartes graphiques modernes, les vecteurs sont généralement des vecteurs qui regroupent plusieurs nombres flottants. De plus, les flottants en question sont des flottants dits simple précision, codés sur 32 bits. Mais il y a quelques exceptions, comme [https://www.realworldtech.com/apple-custom-gpu/ certains GPU d'Apple, qui ne gèrent majoritairement que des flottants codés sur 16 bits], avec des fonctionnalités pour la simple précision. Les anciennes cartes graphiques ne géraient pas du tout de vecteurs contenant des nombres entiers.
===Les instruction scalaires entières, typiques des CPU===
Un processeur SIMD gère donc des instructions SIMD, et les anciennes cartes graphiques ne disposaient que d'instructions de ce type. Mais depuis au moins une décennie, les processeurs de shaders gèrent des instructions normales, non-SIMD. De telles instructions sont appelées des '''instruction scalaires'''. En clair, il s'agit des instructions qu'on retrouve normalement tous les processeurs principaux (les CPU).
Il s'agit généralement d''''instructions entières''', agissent sur des registres entiers non-SIMD. Elles ne traitent pas de vecteur, mais de simples nombres entiers indépendants, sans regroupement d'aucune sorte. Typiquement, il s'agit d'opérations d'addition, de soustraction, des opérations logiques, des comparaisons, guère plus. On trouve aussi des opérations un peu originales, comme des calculs de valeur absolue, du minimum/maximum de deux opérandes, des opérations à prédicat comme une instruction CMOV, etc. Les cartes graphiques supportent rarement la multiplication, mais les plus récentes supportent des multiplications sur des opérandes de 16/32 bits. Par contre, aucune ne gère de division entière.
Les GPU modernes gèrent aussi des instructions de test et de branchement, là encore sur des nombres entiers. Les instructions de test et branchement sont généralement considérées comme à part des instructions de calcul, mais ce sont des opérations scalaires. Les comparaisons se font entre deux entiers scalaires, pas entre deux vecteurs. Retenez bien ce détail, car il sera très important pour la suite.
Les GPU modernes gèrent aussi des '''instructions flottantes scalaires''', à savoir que des instructions qui ont pour opérandes des nombres flottants isolés, qui ne sont pas dans un vecteur. Les processeurs principaux (CPU) d'un ordinateur sont capables de faire beaucoup de calculs arithmétiques simples sur des nombres flottants, comme des additions, des multiplications, des opérations bit-à-bit, éventuellement des divisions, etc. Il en est de même sur les GPUS. Mais ces derniers gèrent aussi de nombreuses instructions flottantes que les CPU n'incorporent presque pas.
Il est rare que les CPU soient capables de faire des opérations flottantes complexes, comme des calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse, etc. De tels calculs sont rares dans les programmes exécutables, alors que les calculs arithmétiques simples y sont légion. Mais le rendu 3D demande pas mal de calculs trigonométriques, de produits scalaires ou d'autres opérations. Par exemple, dans les chapitres précédents, nous avions abordé les calculs d'éclairage et avions vu qu'ils font beaucoup de calculs vectoriels avec des vecteurs comme la normale d'un sommet. Et ces calculs demandent de calculer des produits scalaires et vectoriels, qui eux-mêmes demandent des calculs trigonométriques comme le cosinus ou le sinus.
Aussi, les processeurs de ''shaders'' disposent souvent d'instructions flottantes spécialisées dans les calculs complexes : exponentielle/logarithme, racine carrée, racine carrée inverse, autres. Nous appellerons ces instructions des '''instructions transcendantales''', car elles effectuent des calculs de ce type.
Il faut noter que le processeur incorpore des registres dédiés aux scalaires, séparés des registres SIMD. Par séparés, on veut dire que ce sont des registres différents, adressés différemment, mais qu'ils sont aussi physiquement séparés dans le processeur, ils sont des bancs de registres différents.
===Les instructions en ''co-issue''===
Beaucoup de cartes graphiques récentes comme anciennes incorporent des '''instructions de ''co-issue''''' qui ne se trouvent que sur les cartes graphiques et n'ont aucun équivalent sur les CPUs. Les instructions de ''co-issue'' regroupent plusieurs opérations par instruction. Par exemple, elles peuvent combiner une opération vectorielle avec une opération scalaire. Ou encore, elles peuvent regrouper une opération scalaire, une opération vectorielle et un branchement. Il s'agit d'instructions qui ressemblent grandement à ce qu'on trouve sur les processeurs VLIW.
Un point important est que les cartes graphiques modernes disposent d'instructions à ''co-issue'' en plus des instructions normales. Les instructions à ''co-issue'' sont complémentaire des instructions normales, elles ne les remplacent pas. Les deux peuvent s'utiliser en même temps, dans un même shader. Il a cependant existé des cartes graphiques assez anciennes sur lesquelles toutes les instructions étaient des instructions à ''co-issue'' : certains processeurs de shaders VLIW anciens sont de ce type.
Il y a de nombreuses contraintes quant au regroupement des deux opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre. L'exemple type de ''co-issue'' est la ''co-issue'' entre opérations scalaires et vectorielles : il n'est pas possible de regrouper deux instructions scalaires ou deux instructions vectorielles. La seule possibilité est de regrouper une opération scalaire et une opération vectorielle. La raison à cela est qu'opérations scalaires et vectorielles sont calculées dans des circuits séparés : le processeur incorpore une unité de calcul scalaire et une unité de calcul SIMD, et peut utiliser les deux en parallèle, en même temps. Mais nous verrons cela dans quelques chapitres.
Pour simplifier, cette technique permettait d’exécuter deux opérations arithmétiques en même temps, en parallèle : une opération vectorielle appliquée aux couleurs R, G, et B, et une opération scalaire appliquée à la couleur de transparence. Si cela semble intéressant sur le papier, cela complexifie fortement le processeur de shader, ainsi que la traduction à la volée des shaders en instructions machine.
===Un exemple : le jeu d’instruction du GPU de la Geforce 3===
La première carte graphique commerciale grand public à disposer d'une unité de vertex programmable est la Geforce 3. Celui-ci respectait le format de vertex shader 1.1. L'ensemble des informations à savoir sur cette unité est disponible dans l'article [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p149-lindholm.pdf "A user programmable vertex engine"], disponible sur le net. . Le processeur de cette carte était capable de gérer un seul type de données : les nombres flottants de norme IEEE754. Toutes les informations concernant la coordonnée d'une vertice, voire ses différentes couleurs, doivent être encodées en utilisant ces flottants.
Les processeurs de vertices de la Geforce 3 disposent de registres registres SIMD qui font 128 bits, soit 4 flottants de 32 bits. Elle contient 16 registres d'entrée, 16 registres de sortie, 32 registres généraux. La mémoire des constantes contient 512 "registres".
Le processeur de la Geforce 3 est capable d’exécuter 17 instructions différentes, dont voici les principales :
{|class="wikitable"
|-
!OpCode!!Nom!!Description
|-
! colspan="3" | Opérations mémoire
|-
|MOV||Move||vector -> vector
|-
|ARL||Address register load||miscellaneous
|-
! colspan="3" | Opérations arithmétiques
|-
|ADD||Add||vector -> vector
|-
|MUL||Multiply||vector -> vector
|-
|MAD||Multiply and add||vector -> vector
|-
|MIN||Minimum||vector -> vector
|-
|MAX||Maximum||vector -> vector
|-
|SLT||Set on less than||vector -> vector
|-
|SGE||Set on greater or equal||vector -> vector
|-
|LOG||Log base 2||miscellaneous
|-
|EXP||Exp base 2||miscellaneous
|-
|RCP||Reciprocal||scalar-> replicated scalar
|-
|RSQ||Reciprocal square root||scalar-> replicated scalar
|-
! colspan="3" | Opérations trigonométriques
|-
|DP3||3 term dot product||vector-> replicated scalar
|-
|DP4||4 term dot product||vector-> replicated scalar
|-
|DST||Distance||vector -> vector
|-
! colspan="3" | Opérations d'éclairage géométrique
|-
|LIT||Phong lighting||Calcule l'éclairage de Gouraud
|}
L'instruction la plus intéressante est clairement la dernière : elle éclaire un sommet, en utilisant un éclairage de Phong. Les autres instructions permettent d'implémenter un autre algorithme si besoin, mais cette forme d'éclairage est déjà là à la base.
Les autres instructions sont surtout des instructions arithmétiques : multiplications, additions, exponentielles, logarithmes, racines carrées, etc. Pour les instructions d'accès à la mémoire, on trouve une instruction MOV qui déplace le contenu d'un registre dans un autre et une instruction de calcul d'adresse, mais aucune instruction d'accès à la mémoire sur le processeur de la Geforce 3. Plus tard, les unités de ''vertex shader'' ont acquis la possibilité de lire des données dans une texture.
On remarque que la division est absente. Il faut dire que la contrainte qui veut que toutes ces instructions s’exécutent en un cycle d'horloge pose quelques problèmes avec la division, qui est une opération plutôt lourde en hardware. À la place, on trouve l'instruction RCP, capable de calculer 1/x, avec x un flottant. Cela permet ainsi de simuler une division : pour obtenir Y/X, il suffit de calculer 1/X avec RCP, et de multiplier le résultat par Y.
==La prédication et le SIMT==
Les cartes graphiques récentes peuvent effectuer des branchements, mais ceux-ci sont tout sauf performants. Dès qu'un branchement survient, le processeur est obligé de traiter chaque élément du vecteur un par un, au lieu de tous les traiter en même temps en parallèle. Les performances s'en ressentent, ce qui fait que les branchements sont à éviter le plus possible. Pour améliorer la gestion des conditions, les cartes graphiques modernes incorporent des instructions spécialisées qui permettent de remplacer des codes remplis de branchements par des codes plus simples, compatibles avec l'organisation des données en vecteurs.
Si on met de côté le support de certaines instructions courantes, comme la valeur absolue, ou le calcul du minimum/maximum, la technique la plus importante est la technique dite de '''prédication'''. L'idée est que quand une instruction effectue un calcul sur un ou deux vecteurs, certains éléments du vecteur sont ignorés. Les éléments à ignorer sont choisis suivant le résultat d'une instruction de comparaison, qui effectue un test : les éléments pour lesquels ce test est respecté sont pris en compte, ceux qui ne passent pas le test sont ignorés.
Pour donner un exemple d'utilisation, imaginons que l'on ait un vecteur dans lequel on veut remplacer toutes les valeurs négatives par des 0. Dans ce cas, on utilise :
* une instruction de comparaison, qui compare chaque élément du vecteur avec 0 et génère plusieurs bits de résultat ;
* suivi d'une instruction à prédicat qui met à zéro les éléments pour lesquels les bits de résultat précédents sont à 1.
Elle est implémentée grâce à un registre appelé le '''''Vector Mask Register'''''. Celui-ci permet de stocker des informations qui permettront de sélectionner certaines données et pas d'autres pour faire notre calcul. Il est mis à jour par des instructions de comparaison. le ''Vector Mask Register'' stocke un bit pour chaque flottant présent dans le vecteur à traiter, bit qui indique s'il faut appliquer l'instruction sur ce flottant. Si ce bit est à 1, notre instruction doit s’exécuter sur la donnée associée à ce bit. Sinon, notre instruction ne doit pas la modifier. On peut ainsi traiter seulement une partie des registres stockant des vecteurs SIMD.
[[File:Vector mask register.png|centre|vignette|upright=2.0|''Vector mask register'']]
===La prédication avec une pile SIMT===
Au niveau du jeu d’instruction, les architectures SIMT implémentent de la prédication, sous une forme améliorée. Les processeurs SIMT actuels sont surtout utilisées sur les processeurs intégrés aux cartes graphiques. Et ces derniers gèrent très mal les branchements, et encore : beaucoup de cartes graphiques, même récentes, ne gèrent tout simplement pas les branchements. Elles doivent donc se débrouiller avec uniquement la prédication, là où les processeurs SIMD utilisent des branchements normaux en complément de la prédication. Insistons sur le fait que cet usage exclusif de la prédication n'est présent que sur une sous-partie des architectures SIMT, le seul exemple que l'auteur de ce wikilivre connait étant celui des cartes graphiques.
Les architectures SIMT sans branchements doivent donc trouver des solutions pour gérer les structures de contrôle imbriquées, à savoir une boucle placée à l'intérieur d'une autre boucle, un IF...ELSE dans un autre IF...ELSE, etc. Elles utilisent pour cela la prédication, combinée avec des mécanismes annexes. Le premier d'entre eux est l'usage de plusieurs registres de masques organisés d'une manière bien précise, l'autre est l'usage de compteurs d'activité. Voyons ces deux techniques.
La '''pile de masques''' remplace le ou les registres de masque. Sans elle, le processeur SIMD incorpore un registre de masque qui est adressé implicitement ou explicitement. Éventuellement, le processeur peut contenir plusieurs registres de masque séparés adressables via un nom de registre. Avec elle, le processeur SIMD incorpore plusieurs registres de masque organisé en pile. Le registre de masque est donc remplacé par une mémoire LIFO, une pile, dans laquelle plusieurs masques sont empilés.
Le tout forme une pile, similaire à la pile d'appel, sauf qu'elle est utilisée pour empiler des masques. Un masque est calculé et empilé à chaque entrée dans une structure de contrôle, puis dépilé une fois la structure de contrôle exécutée. L'empilement et le dépilement des masques est effectué par des instructions PUSH et POP, présentes dans le jeu d'instruction du processeur SIMD.
Le calcul des masques doit répondre à plusieurs impératifs.
* Premièrement, chaque masque se calcule en faisant un ET entre le masque précédent et le masque calculé par l'instruction de test. Cela permet de ne pas réveiller d’élément au beau milieu d'une structure imbriquée. Si in IF désactive certains éléments du vecteur, une condition imbriquée dans ce IF ne doit pas réveiller cet élément. Le fait de faire un ET entre les masques garantit cela.
* Deuxièmement, les masques doivent être empilés et dépilés correctement. Au moment de rentrer dans une structure de contrôle, on effectue une instruction de test associée à la structure de contrôle, qui calcule un masque, et on empile le masque calculé. Au moment de sortir de la structure de contrôle, on dépile le masque en question.
L'implémentation demande d'utiliser une mémoire LIFO pour stocker la pile de masques, et quelques circuits annexes. Il faut notamment un circuit relié à l'ALU qui récupère les conditions, les résultats des comparaisons, et qui effectue le ET pour combiner les masques.
Pour donner un exemple, prenons le code suivant, qui est volontairement simpliste et ne sert qu'à des fins d'explication :
<syntaxhighlight lang="c">
if ( condition 1 )
{
if ( condition 2 )
{
...
}
else
{
...
}
Autres instructions
}
Instructions après le IF...
</syntaxhighlight>
Imaginons que l'on traite des vecteurs de 8 éléments.
Pour le vecteur considéré, la première condition (a > 0) n'est respectée que par les 4 premiers éléments. L'instruction de condition calcule alors le masque correspondant : 1111 0000. Le masque est alors calculé, puis empilé au sommet de la pile.
La seconde instruction de test, qui teste la variable b, est maintenant valide pour les 4 bits du milieu du masque. Mais n'allez pas croire que le masque correspondant soit 0011 11100 : il faut tenir compte de la condition précédente, qui a éliminé les 4 derniers éléments. Pour cela, on fait un ET logique entre le masque précédent, et le masque calculé par la condition. Le masque au sommet de la pile est donc lu, combiné avec le masque calculé par l'instruction, ce qui donne le masque final. Le masque final est alors empilé au sommet de la pile.
On exécute alors l'instruction du IF, en tenant compte du masque qui est au sommet de la pile. Si le IF était plus compliqué, toutes les instructions suivantes tiendraient compte du masque. En fait, le masque est pris en compte tant qu'il n'est pas dépilé. Une fois que le IF est terminé, le masque est dépilé.
On passe alors au ELSE, et rebelotte. Le masque pour le ELSE est calculé en combinant le masque au sommet de la pile avec la condition du ELSE. Le masque au sommet de la pile est celui calculé à l'entrée du premier IF, pas le second qui a été dépilé. Les instructions du ELSE sont alors exécutées en tenant compte de ce masque. Une fois qu'elles sont toutes exécutées, le masque est dépilé.
Puis vient l'exécution des instructions après le ELSE. Elles utilisent le masque empilé au sommet de la pile, qui correspond à celui à l'entrée du IF.
Puis vient le moment d'exécuter les instructions après le IF : pas de masque, on exécute sur tout le vecteur.
===Les compteurs d'activité===
Une variante de la technique précédente remplace la pile de masques par des '''compteurs d'activité'''. La technique est similaire, si ce n'est qu'elle utilise moins de circuits. Avant , on avait une pile de masques de même taille, dont les bits sont à 0 ou 1 suivant que la condition est remplie. La pile de masque ressemble donc à ceci :
{|class="wikitable"
|-
! masque 1
| 1 || 1 || 1 || 1
|-
! masque 2
| 0 || 1 || 1 || 1
|-
! masque 3
| 0 || 1 || 1 || 1
|-
! masque 4
| 0 || 0 || 0 || 1
|-
! masque 1
| colspan="4" | vide
|}
Une manière équivalente de représenter cette pile de masque est de compter combien de bits sont à 0 dans chaque colonne. Attention : j'ai bien dit à 0 ! On obtient alors :
{|class="wikitable"
|-
! masque 1
| 3 || 1 || 1 || 0
|}
Et c'est le principe caché derrière la technique des compteurs d'activité. Chaque élément dans un vecteur, chaque place, se voit attribuer un compteur. Un compteur non-nul indique qu'il ne faut pas prendre en compte l’élément. Ce n'est qu'une fois que le compteur est nul que l'on effectue des opérations sur l’élément associé du vecteur.
À chaque fois qu'on entre dans une structure de contrôle, on teste une condition sur chaque élément. Si la condition est respectée pour un élément, alors le compteur ne change pas. Mais si la condition n'est pas respectée, alors on incrémente le compteur associé. En sortant de la structure de contrôle, on décrémente le compteur associé. Notons que les compteurs qui n'ont pas été incrémentés en entrant dans la structure de contrôle ne sont pas décrémentés en sortant. En clair, là où on empilait/dépilait un masque, on se contente d'incrémenter/décrémenter un compteur.
Utiliser un compteur en lieu et place d'une colonne entière dans la pile de masque utilise moins de bits. Et c'est sans doute pour cette raison que certaines cartes graphiques, comme les cartes graphiques intégrées d'Intel depuis 2004, utilisent cette technique.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes accélératrices 3D
| prevText=Les cartes accélératrices 3D
| next=La microarchitecture des processeurs de shaders
| nextText=La microarchitecture des processeurs de shaders
}}
{{autocat}}
hbjuoge66nttavmwj62bscvb2flvq3m
763432
763431
2026-04-10T23:50:15Z
Mewtow
31375
/* Les registres des processeurs de shaders */
763432
wikitext
text/x-wiki
Les '''''shaders''''' sont des programmes informatiques exécutés par la carte graphique, et plus précisément par des processeurs de ''shaders''. Un point très important à comprendre est que chaque triangle ou pixel d'une scène 3D peut être traité indépendamment des autres. Le tout se résume comme suit :
: '''L’exécution d'un shader génère un grand nombre d'instances de ce shader, chacune traitant un paquet de pixels/sommets différent.'''
En conséquence, il est possible de traiter chaque instance d'un ''shader'' en parallèle des autres, en même temps, au lieu de traiter les instances l'une après l'autre.
La conséquence est que les cartes graphiques sont des architectures massivement parallèles, à savoir qu'elles sont capables d'effectuer un grand nombre de calculs indépendants en même temps. De plus, le parallélisme utilisé est du parallélisme de données, à savoir qu'on exécute le même programme sur des données différentes, chaque donnée étant traitée en parallèle des autres. Les cartes graphiques récentes incorporent toutes les techniques de parallélisme de donnée au niveau matériel, et nous allons toutes les détailler dans ce chapitre. S'il fallait résumer, elles ont plusieurs processeurs/cœurs, chaque cœur est capable d’exécuter des instructions SIMD (ils ne font que cela, à vrai dire), les cœurs sont fortement multithreadés, et j'en passe.
[[File:CPU and GPU.png|vignette|Comparaison du nombre de processeurs et de cœurs entre CPU et GPU.]]
Le premier point est qu'une carte graphique contient de nombreux processeurs, qui eux-mêmes contiennent plusieurs unités de calcul. Savoir combien de cœurs contient une carte graphique est cependant très compliqué, car la terminologie utilisée par les fabricants de carte graphique est particulièrement confuse. Il n'est pas rare que ceux-ci appellent cœurs ou processeurs, ce qui correspond en réalité à une unité de calcul d'un processeur normal, sans doute histoire de gonfler les chiffres. Et on peut généraliser à la majorité de la terminologie utilisée par les fabricants, que ce soit pour les termes ''warps processor'', ou autre, qui ne sont pas aisés à interpréter.
L'architecture d'une carte graphique récente est illustrée ci-dessous. Rien de bien déroutant pour qui a déjà étudié les architectures à parallélisme de données, mais quelques rappels ou explications ne peuvent pas faire de mal. Le premier point est la présence d'un grand nombre de processeurs/cœurs, les rectangles en bleu/rouge. Chacun d'entre eux contient un grand nombre de circuits de calculs, avec des circuits de calcul simples mais nombreux en rouge, et une unité pour les calculs complexes (trigonométriques, racines carrées, autres) en rouge. Le tout est relié à une hiérarchie mémoire indiquée en vert, comprenant des mémoires locales en complément de la mémoire vidéo principale. Le tout est alimenté par une unité de répartition, le '''''Thread Execution Control Unit''''' en jaune, qui répartit les différentes instances du ''shader'' sur les différents processeurs. Elle est aussi appelée le '''processeur de commandes''', comme nous le verrons dans quelques chapitres. Nous utiliserons le terme processeur de commande dans ce qui suit.
[[File:NVIDIA GPU Accelerator Block Diagram.png|centre|vignette|upright=2.5|Ce schéma illustre l'architecture d'un GPU en utilisant la terminologie NVIDIA. Comme on le voit, la carte graphique contient plusieurs cœurs de processeur distincts. Chacun d'entre eux contient plusieurs unités de calcul généralistes, appelées processeurs de threads, qui s'occupent de calculs simples (en bleu). D'autres calculs plus complexes sont pris en charge par une unité de calcul spécialisée (en rouge). Ces cœurs sont alimentés en instructions par le processeur de commandes, ici appelé ''Thread Execution Control Unit'', qui répartit les différents shaders sur chaque cœur. Enfin, on voit que chaque cœur a accès à une mémoire locale dédiée, en plus d'une mémoire vidéo partagée entre tous les cœurs.]]
Les portions bleu, jaune et verte du schéma précédent méritent chacune un chapitre séparé. La hiérarchie mémoire en vert fera l'objet d'un chapitre ultérieur. Quant au répartiteur en jaune, il sera détaillé en profondeur dans le prochain chapitre. Dans ce chapitre, nous allons voir comment fonctionnent les processeurs de ''shaders'', la partie bleue. Nous allons voir que ceux-ci ne sont pas très différents des processeurs que l'on trouve dans les ordinateurs normaux, du moins dans les grandes lignes. Ce sont des processeurs séquentiels, qui exécutent des instructions les unes après les autres. Ils ont des instructions machines, des modes d'adressage, un assembleur, des registres et tout ce qui fait qu'un processeur est un processeur. Néanmoins, il y a une différence de taille : ce sont des processeurs adaptés pour effectuer un grand nombre de calculs en parallèle.
==Les registres des processeurs de shaders==
Un processeur de shaders contient beaucoup de registres, sans quoi il ne pourrait pas faire son travail efficacement. Les plus intuitifs sont les '''registres généraux''', aussi appelés registres temporaires, qui servent à mémoriser n'importe quelle donnée utilisable par le processeur. Ils servent un peu à tout, le processeur peut les manipuler à loisir. Tout processeur digne de ce nom en possède. Mais un processeur de ''shader'' dispose aussi de registres spécialisés, qu'on ne trouve que sur les processeurs de ''shaders'', qui servent à l'interfacer avec le reste du pipeline graphique.
[[File:Architecture carte graphique vertex avec texture.PNG|centre|vignette|upright=2|Architecture carte graphique vertex avec texture]]
===Les registres d'interface avec le pipeline graphique===
Les processeurs de ''vertex shader'' reçoivent des 'sommets provenant de l'''input assembler'' et envoient leur résultat au rastériseur. Les processeurs de ''pixel shader'' reçoit des données provenant de l'unité de rastérisation; et envoie son résultat final aux ROPs. Et pour cela, le processeur de shader a des registres dédiés, qui servent d'interface avec le reste du pipeline graphique.
Les '''registres d'entrée''' réceptionnent soit les sommets provenant de l'''input assembler'', soit les pixels provenant de l'unité de rastérisation. Les registres d'entrée sont en lecture seule, du point de vue du processeur de shader, seule l'unité de rastérisation peut écrire dedans. Ils sont initialisés avant l'exécution de l'instance du ''shader''.
Les '''registres de sortie''' sont là où le processeur stocke les résultats à envoyer, soit au rastériseur pour un ''vertex shader'', soit aux ROP pour un ''pixel shader''. Les registres de sorties sont en écriture seule. Pour donner un exemple, les ''vertex shaders'' ont au minimum un registre pour la position du sommet dans l'espace (trois coordonnées), un autre pour la couleur/luminosité du sommet, un autre pour la couleur du brouillard, un autre pour les coordonnées de texture.
{|class="wikitable"
|+ Registres de sortie des ''pixel/vertex shaders''
|-
! Vertex shader
! Pixel shader
|-
| Couleur du pixel
| Couleur du sommet
|-
| Profondeur du pixel
| Position du sommet
|-
| rowspan="2" |
| Coordonnées de texture du sommet
|-
| Couleur de brouillard.
|}
===Les registres spécialisés internes===
D'autres registres spécialisés ne font pas l'interface avec le reste du GPU. Ils servent à stocker des constantes ou des données importantes, qui n'ont pas vraiment leur place dans les registres généraux.
Les '''registres de constantes''' servent pour stocker des constantes utiles pour le ''shader''. Par exemple, pour les ''vertex shaders'', ils stockent les matrices servant aux différentes étapes de transformation ou d'éclairage. Ces constantes sont placées dans ces registres peu après le chargement du vertex shader dans la mémoire vidéo. Toutefois, le vertex shader peut écrire dans ces registres, au prix d'une perte de performance particulièrement violente.
Les ''pixel/vertex shaders'' 1.0 ne géraient que des constantes flottantes pour les ''vertex shaders'', entières pour les ''pixel shaders''. Mais les ''pixel/vertex shaders'' 2.0 et 3.0 avaient des registres de constantes séparés pour les nombres entiers, les nombres flottants, et même les nombres booléens. Les constantes entières et booléennes étaient utilisées pour gérer les boucles, guère plus. Aussi, il y en avait 16, comparé aux centaines de registres de constantes flottants. Mais avec les ''pixel/vertex shaders'' 4.0 et plus, les registres de constante ont été fusionnés et n'ont plus de type prédéterminé, le programmeur gère ces registres comme il l'entend.
L'adressage des registres de constante est quelque peu particulier. Il faut dire qu'il y en a plusieurs milliers sur les processeurs de ''shaders'' modernes, au point qu'il serait plus juste de parler de mémoire RAM des constantes. Les registres de constante sont en effet un ''local store'' un peu spécial, intégré directement dans le processeur. Et le processeur accède à ce ''local store'' en utilisant une mode d'adressage semblable à celui utilisé pour la mémoire, avec un mode d'adressage indirect. L'adresse à lire dans ce ''local store'' est dans un registre, séparé du reste, appelé le '''registre d'adresse de constante'''.
Depuis les ''pixel/vertex shaders'' 3.0, les ''shaders'' sont capables d'effectuer des boucles et d'autres structures de contrôle familières pour les programmeurs. Et deux registres ont été intégrés afin d'améliorer les performances des structures de contrôle. Le premier est un registre à prédicat, qui sera vu dans la section sur le SIMD avec prédication. Le second est un '''registre compteur de boucle''', qui mémorise l'indice d'une boucle. Il est initialisé à 0, et est incrémenté à chaque fois qu'une boucle s'exécute.
Certains processeurs de shader ont aussi des '''registres de texture''' , qui servent d'interface avec la mémoire pour la gestion des textures. Ils mémorisent les texels lus par l'unité de texture. L'unité de texture lit un texel, plusieurs avec ''multitexturing'', et les place dans ces registres de texture. Les registres de texture sont parfois initialisés avant l'exécution du ''shader'', mais la plupart sont initialisé quand le ''shader'' termine une instruction de lecture de texture. Ils sont généralement en lecture seule, mais il y a des exceptions.
==Les processeurs de shaders modernes : les processeurs SIMD==
Maintenant, voyons quelles sont les instructions supportées par les processeurs de shaders modernes. Et si je dis moderne, c'est car nous ne parlerons que des GPU de l'époque DirectX 10 et après, pas des GPU de l'époque DirectX 9 et antérieur. La raison est que le jeu d'instruction des shaders a franchement évolué, avec le passage d'architectures VLIW à des architectures SIMD. Et cela a eu des conséquences assez profondes sur le jeu d'instruction et leur microarchitecture. Nous n'allons parler des GPU de type SIMD dans ce chapitre. Un chapitre dédié sera consacré aux GPU de type VLIW.
Le jeu d'instruction des GPU NVIDIA n'est pas encore connu à l'heure où j'écris ces lignes, la documentation du constructeur n'est pas disponible. Quelques chercheurs ont tenté de faire de la rétro-ingénierie du code de divers shaders pour retrouver le jeu d'instruction des divers GPU NVIDIA, ce qui fait qu'on a cependant une idée de ce dernier. Mais rien d'officiel. Par contre, AMD fournit librement cette documentation sur le net. Ce qui fait qu'on peut trouver des documents de ce genre :
* [https://developer.amd.com/wordpress/media/2012/12/AMD_Southern_Islands_Instruction_Set_Architecture.pdf Graphics Core Next 1 instruction set] ;
* [https://developer.amd.com/wordpress/media/2013/07/AMD_Sea_Islands_Instruction_Set_Architecture.pdf Graphics Core Next 2 instruction set] ;
* [https://developer.amd.com/wordpress/media/2013/12/AMD_GCN3_Instruction_Set_Architecture_rev1.1.pdf Graphics Core Next 3 and 4 instruction sets] ;
* [https://developer.amd.com/wp-content/resources/Vega_Shader_ISA_28July2017.pdf Graphics Core Next 5 instruction set] ;
* [https://developer.amd.com/wp-content/resources/Vega_7nm_Shader_ISA.pdf "Vega" 7nm instruction set architecture] (also referred to as Graphics Core Next 5.1) ;
* [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna3-shader-instruction-set-architecture-feb-2023_0.pdf Jeu d'instruction des GPU de type RDNA3 d'AMD].
===Les instructions SIMD===
Les '''instructions SIMD''' manipulent plusieurs nombres en même temps. Elles manipulent plus précisément des '''vecteurs''', des ensembles de plusieurs nombres entiers ou nombres flottants placés les uns à côté des autres, le tout ayant une taille fixe, qui sont stockés dans des registres spécialisés. En général, tous les vecteurs ont une taille fixe, peu importe leur contenu. Cela implique que suivant la taille des données à manipuler, on pourra en placer plus ou moins dans un vecteur. Par exemple, un vecteur de 128 bits pourra contenir 4 entiers de 32 bits, 4 flottants 32 bits, ou 8 entiers de 16 bits.
[[File:Vector register.png|centre|vignette|upright=2|Contenu d'un vecteur en fonction du type de données utilisé.]]
Les vecteurs sont stockés dans des '''registres vectoriels''', aussi appelés '''registres SIMD'''. Un registre vectoriel peut contenir un vecteur complet, pas plus. En conséquence, ils ont une taille assez importante : ils font généralement 128, 256, voire 512 bits, comparé aux 32/64 bits des registres des CPU. Les cartes graphiques modernes contiennent un très grand nombre de registres SIMD.
{|
|+ Comparaison entre un processeur sans registres vectoriels, et avec registres vectoriels.
|[[File:Non-SIMD cpu diagram1.svg|vignette|upright=1.5|CPU Non-SIMD]]
|[[File:SIMD cpu diagram1.svg|vignette|upright=1.5|CPU SIMD]]
|}
Une instruction SIMD traite chaque donnée du vecteur indépendamment des autres. Par exemple, une instruction d'addition vectorielle va additionner ensemble les données qui sont à la même place dans deux vecteurs, et placer le résultat dans un autre vecteur, à la même place.
[[File:Instructions SIMD.png|centre|vignette|upright=2.0|Instructions SIMD]]
Sur les cartes graphiques modernes, les vecteurs sont généralement des vecteurs qui regroupent plusieurs nombres flottants. De plus, les flottants en question sont des flottants dits simple précision, codés sur 32 bits. Mais il y a quelques exceptions, comme [https://www.realworldtech.com/apple-custom-gpu/ certains GPU d'Apple, qui ne gèrent majoritairement que des flottants codés sur 16 bits], avec des fonctionnalités pour la simple précision. Les anciennes cartes graphiques ne géraient pas du tout de vecteurs contenant des nombres entiers.
===Les instruction scalaires entières, typiques des CPU===
Un processeur SIMD gère donc des instructions SIMD, et les anciennes cartes graphiques ne disposaient que d'instructions de ce type. Mais depuis au moins une décennie, les processeurs de shaders gèrent des instructions normales, non-SIMD. De telles instructions sont appelées des '''instruction scalaires'''. En clair, il s'agit des instructions qu'on retrouve normalement tous les processeurs principaux (les CPU).
Il s'agit généralement d''''instructions entières''', agissent sur des registres entiers non-SIMD. Elles ne traitent pas de vecteur, mais de simples nombres entiers indépendants, sans regroupement d'aucune sorte. Typiquement, il s'agit d'opérations d'addition, de soustraction, des opérations logiques, des comparaisons, guère plus. On trouve aussi des opérations un peu originales, comme des calculs de valeur absolue, du minimum/maximum de deux opérandes, des opérations à prédicat comme une instruction CMOV, etc. Les cartes graphiques supportent rarement la multiplication, mais les plus récentes supportent des multiplications sur des opérandes de 16/32 bits. Par contre, aucune ne gère de division entière.
Les GPU modernes gèrent aussi des instructions de test et de branchement, là encore sur des nombres entiers. Les instructions de test et branchement sont généralement considérées comme à part des instructions de calcul, mais ce sont des opérations scalaires. Les comparaisons se font entre deux entiers scalaires, pas entre deux vecteurs. Retenez bien ce détail, car il sera très important pour la suite.
Les GPU modernes gèrent aussi des '''instructions flottantes scalaires''', à savoir que des instructions qui ont pour opérandes des nombres flottants isolés, qui ne sont pas dans un vecteur. Les processeurs principaux (CPU) d'un ordinateur sont capables de faire beaucoup de calculs arithmétiques simples sur des nombres flottants, comme des additions, des multiplications, des opérations bit-à-bit, éventuellement des divisions, etc. Il en est de même sur les GPUS. Mais ces derniers gèrent aussi de nombreuses instructions flottantes que les CPU n'incorporent presque pas.
Il est rare que les CPU soient capables de faire des opérations flottantes complexes, comme des calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse, etc. De tels calculs sont rares dans les programmes exécutables, alors que les calculs arithmétiques simples y sont légion. Mais le rendu 3D demande pas mal de calculs trigonométriques, de produits scalaires ou d'autres opérations. Par exemple, dans les chapitres précédents, nous avions abordé les calculs d'éclairage et avions vu qu'ils font beaucoup de calculs vectoriels avec des vecteurs comme la normale d'un sommet. Et ces calculs demandent de calculer des produits scalaires et vectoriels, qui eux-mêmes demandent des calculs trigonométriques comme le cosinus ou le sinus.
Aussi, les processeurs de ''shaders'' disposent souvent d'instructions flottantes spécialisées dans les calculs complexes : exponentielle/logarithme, racine carrée, racine carrée inverse, autres. Nous appellerons ces instructions des '''instructions transcendantales''', car elles effectuent des calculs de ce type.
Il faut noter que le processeur incorpore des registres dédiés aux scalaires, séparés des registres SIMD. Par séparés, on veut dire que ce sont des registres différents, adressés différemment, mais qu'ils sont aussi physiquement séparés dans le processeur, ils sont des bancs de registres différents.
===Les instructions en ''co-issue''===
Beaucoup de cartes graphiques récentes comme anciennes incorporent des '''instructions de ''co-issue''''' qui ne se trouvent que sur les cartes graphiques et n'ont aucun équivalent sur les CPUs. Les instructions de ''co-issue'' regroupent plusieurs opérations par instruction. Par exemple, elles peuvent combiner une opération vectorielle avec une opération scalaire. Ou encore, elles peuvent regrouper une opération scalaire, une opération vectorielle et un branchement. Il s'agit d'instructions qui ressemblent grandement à ce qu'on trouve sur les processeurs VLIW.
Un point important est que les cartes graphiques modernes disposent d'instructions à ''co-issue'' en plus des instructions normales. Les instructions à ''co-issue'' sont complémentaire des instructions normales, elles ne les remplacent pas. Les deux peuvent s'utiliser en même temps, dans un même shader. Il a cependant existé des cartes graphiques assez anciennes sur lesquelles toutes les instructions étaient des instructions à ''co-issue'' : certains processeurs de shaders VLIW anciens sont de ce type.
Il y a de nombreuses contraintes quant au regroupement des deux opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre. L'exemple type de ''co-issue'' est la ''co-issue'' entre opérations scalaires et vectorielles : il n'est pas possible de regrouper deux instructions scalaires ou deux instructions vectorielles. La seule possibilité est de regrouper une opération scalaire et une opération vectorielle. La raison à cela est qu'opérations scalaires et vectorielles sont calculées dans des circuits séparés : le processeur incorpore une unité de calcul scalaire et une unité de calcul SIMD, et peut utiliser les deux en parallèle, en même temps. Mais nous verrons cela dans quelques chapitres.
Pour simplifier, cette technique permettait d’exécuter deux opérations arithmétiques en même temps, en parallèle : une opération vectorielle appliquée aux couleurs R, G, et B, et une opération scalaire appliquée à la couleur de transparence. Si cela semble intéressant sur le papier, cela complexifie fortement le processeur de shader, ainsi que la traduction à la volée des shaders en instructions machine.
===Un exemple : le jeu d’instruction du GPU de la Geforce 3===
La première carte graphique commerciale grand public à disposer d'une unité de vertex programmable est la Geforce 3. Celui-ci respectait le format de vertex shader 1.1. L'ensemble des informations à savoir sur cette unité est disponible dans l'article [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p149-lindholm.pdf "A user programmable vertex engine"], disponible sur le net. . Le processeur de cette carte était capable de gérer un seul type de données : les nombres flottants de norme IEEE754. Toutes les informations concernant la coordonnée d'une vertice, voire ses différentes couleurs, doivent être encodées en utilisant ces flottants.
Les processeurs de vertices de la Geforce 3 disposent de registres registres SIMD qui font 128 bits, soit 4 flottants de 32 bits. Elle contient 16 registres d'entrée, 16 registres de sortie, 32 registres généraux. La mémoire des constantes contient 512 "registres".
Le processeur de la Geforce 3 est capable d’exécuter 17 instructions différentes, dont voici les principales :
{|class="wikitable"
|-
!OpCode!!Nom!!Description
|-
! colspan="3" | Opérations mémoire
|-
|MOV||Move||vector -> vector
|-
|ARL||Address register load||miscellaneous
|-
! colspan="3" | Opérations arithmétiques
|-
|ADD||Add||vector -> vector
|-
|MUL||Multiply||vector -> vector
|-
|MAD||Multiply and add||vector -> vector
|-
|MIN||Minimum||vector -> vector
|-
|MAX||Maximum||vector -> vector
|-
|SLT||Set on less than||vector -> vector
|-
|SGE||Set on greater or equal||vector -> vector
|-
|LOG||Log base 2||miscellaneous
|-
|EXP||Exp base 2||miscellaneous
|-
|RCP||Reciprocal||scalar-> replicated scalar
|-
|RSQ||Reciprocal square root||scalar-> replicated scalar
|-
! colspan="3" | Opérations trigonométriques
|-
|DP3||3 term dot product||vector-> replicated scalar
|-
|DP4||4 term dot product||vector-> replicated scalar
|-
|DST||Distance||vector -> vector
|-
! colspan="3" | Opérations d'éclairage géométrique
|-
|LIT||Phong lighting||Calcule l'éclairage de Gouraud
|}
L'instruction la plus intéressante est clairement la dernière : elle éclaire un sommet, en utilisant un éclairage de Phong. Les autres instructions permettent d'implémenter un autre algorithme si besoin, mais cette forme d'éclairage est déjà là à la base.
Les autres instructions sont surtout des instructions arithmétiques : multiplications, additions, exponentielles, logarithmes, racines carrées, etc. Pour les instructions d'accès à la mémoire, on trouve une instruction MOV qui déplace le contenu d'un registre dans un autre et une instruction de calcul d'adresse, mais aucune instruction d'accès à la mémoire sur le processeur de la Geforce 3. Plus tard, les unités de ''vertex shader'' ont acquis la possibilité de lire des données dans une texture.
On remarque que la division est absente. Il faut dire que la contrainte qui veut que toutes ces instructions s’exécutent en un cycle d'horloge pose quelques problèmes avec la division, qui est une opération plutôt lourde en hardware. À la place, on trouve l'instruction RCP, capable de calculer 1/x, avec x un flottant. Cela permet ainsi de simuler une division : pour obtenir Y/X, il suffit de calculer 1/X avec RCP, et de multiplier le résultat par Y.
==La prédication et le SIMT==
Les cartes graphiques récentes peuvent effectuer des branchements, mais ceux-ci sont tout sauf performants. Dès qu'un branchement survient, le processeur est obligé de traiter chaque élément du vecteur un par un, au lieu de tous les traiter en même temps en parallèle. Les performances s'en ressentent, ce qui fait que les branchements sont à éviter le plus possible. Pour améliorer la gestion des conditions, les cartes graphiques modernes incorporent des instructions spécialisées qui permettent de remplacer des codes remplis de branchements par des codes plus simples, compatibles avec l'organisation des données en vecteurs.
Si on met de côté le support de certaines instructions courantes, comme la valeur absolue, ou le calcul du minimum/maximum, la technique la plus importante est la technique dite de '''prédication'''. L'idée est que quand une instruction effectue un calcul sur un ou deux vecteurs, certains éléments du vecteur sont ignorés. Les éléments à ignorer sont choisis suivant le résultat d'une instruction de comparaison, qui effectue un test : les éléments pour lesquels ce test est respecté sont pris en compte, ceux qui ne passent pas le test sont ignorés.
Pour donner un exemple d'utilisation, imaginons que l'on ait un vecteur dans lequel on veut remplacer toutes les valeurs négatives par des 0. Dans ce cas, on utilise :
* une instruction de comparaison, qui compare chaque élément du vecteur avec 0 et génère plusieurs bits de résultat ;
* suivi d'une instruction à prédicat qui met à zéro les éléments pour lesquels les bits de résultat précédents sont à 1.
Elle est implémentée grâce à un registre appelé le '''''Vector Mask Register'''''. Celui-ci permet de stocker des informations qui permettront de sélectionner certaines données et pas d'autres pour faire notre calcul. Il est mis à jour par des instructions de comparaison. le ''Vector Mask Register'' stocke un bit pour chaque flottant présent dans le vecteur à traiter, bit qui indique s'il faut appliquer l'instruction sur ce flottant. Si ce bit est à 1, notre instruction doit s’exécuter sur la donnée associée à ce bit. Sinon, notre instruction ne doit pas la modifier. On peut ainsi traiter seulement une partie des registres stockant des vecteurs SIMD.
[[File:Vector mask register.png|centre|vignette|upright=2.0|''Vector mask register'']]
===La prédication avec une pile SIMT===
Au niveau du jeu d’instruction, les architectures SIMT implémentent de la prédication, sous une forme améliorée. Les processeurs SIMT actuels sont surtout utilisées sur les processeurs intégrés aux cartes graphiques. Et ces derniers gèrent très mal les branchements, et encore : beaucoup de cartes graphiques, même récentes, ne gèrent tout simplement pas les branchements. Elles doivent donc se débrouiller avec uniquement la prédication, là où les processeurs SIMD utilisent des branchements normaux en complément de la prédication. Insistons sur le fait que cet usage exclusif de la prédication n'est présent que sur une sous-partie des architectures SIMT, le seul exemple que l'auteur de ce wikilivre connait étant celui des cartes graphiques.
Les architectures SIMT sans branchements doivent donc trouver des solutions pour gérer les structures de contrôle imbriquées, à savoir une boucle placée à l'intérieur d'une autre boucle, un IF...ELSE dans un autre IF...ELSE, etc. Elles utilisent pour cela la prédication, combinée avec des mécanismes annexes. Le premier d'entre eux est l'usage de plusieurs registres de masques organisés d'une manière bien précise, l'autre est l'usage de compteurs d'activité. Voyons ces deux techniques.
La '''pile de masques''' remplace le ou les registres de masque. Sans elle, le processeur SIMD incorpore un registre de masque qui est adressé implicitement ou explicitement. Éventuellement, le processeur peut contenir plusieurs registres de masque séparés adressables via un nom de registre. Avec elle, le processeur SIMD incorpore plusieurs registres de masque organisé en pile. Le registre de masque est donc remplacé par une mémoire LIFO, une pile, dans laquelle plusieurs masques sont empilés.
Le tout forme une pile, similaire à la pile d'appel, sauf qu'elle est utilisée pour empiler des masques. Un masque est calculé et empilé à chaque entrée dans une structure de contrôle, puis dépilé une fois la structure de contrôle exécutée. L'empilement et le dépilement des masques est effectué par des instructions PUSH et POP, présentes dans le jeu d'instruction du processeur SIMD.
Le calcul des masques doit répondre à plusieurs impératifs.
* Premièrement, chaque masque se calcule en faisant un ET entre le masque précédent et le masque calculé par l'instruction de test. Cela permet de ne pas réveiller d’élément au beau milieu d'une structure imbriquée. Si in IF désactive certains éléments du vecteur, une condition imbriquée dans ce IF ne doit pas réveiller cet élément. Le fait de faire un ET entre les masques garantit cela.
* Deuxièmement, les masques doivent être empilés et dépilés correctement. Au moment de rentrer dans une structure de contrôle, on effectue une instruction de test associée à la structure de contrôle, qui calcule un masque, et on empile le masque calculé. Au moment de sortir de la structure de contrôle, on dépile le masque en question.
L'implémentation demande d'utiliser une mémoire LIFO pour stocker la pile de masques, et quelques circuits annexes. Il faut notamment un circuit relié à l'ALU qui récupère les conditions, les résultats des comparaisons, et qui effectue le ET pour combiner les masques.
Pour donner un exemple, prenons le code suivant, qui est volontairement simpliste et ne sert qu'à des fins d'explication :
<syntaxhighlight lang="c">
if ( condition 1 )
{
if ( condition 2 )
{
...
}
else
{
...
}
Autres instructions
}
Instructions après le IF...
</syntaxhighlight>
Imaginons que l'on traite des vecteurs de 8 éléments.
Pour le vecteur considéré, la première condition (a > 0) n'est respectée que par les 4 premiers éléments. L'instruction de condition calcule alors le masque correspondant : 1111 0000. Le masque est alors calculé, puis empilé au sommet de la pile.
La seconde instruction de test, qui teste la variable b, est maintenant valide pour les 4 bits du milieu du masque. Mais n'allez pas croire que le masque correspondant soit 0011 11100 : il faut tenir compte de la condition précédente, qui a éliminé les 4 derniers éléments. Pour cela, on fait un ET logique entre le masque précédent, et le masque calculé par la condition. Le masque au sommet de la pile est donc lu, combiné avec le masque calculé par l'instruction, ce qui donne le masque final. Le masque final est alors empilé au sommet de la pile.
On exécute alors l'instruction du IF, en tenant compte du masque qui est au sommet de la pile. Si le IF était plus compliqué, toutes les instructions suivantes tiendraient compte du masque. En fait, le masque est pris en compte tant qu'il n'est pas dépilé. Une fois que le IF est terminé, le masque est dépilé.
On passe alors au ELSE, et rebelotte. Le masque pour le ELSE est calculé en combinant le masque au sommet de la pile avec la condition du ELSE. Le masque au sommet de la pile est celui calculé à l'entrée du premier IF, pas le second qui a été dépilé. Les instructions du ELSE sont alors exécutées en tenant compte de ce masque. Une fois qu'elles sont toutes exécutées, le masque est dépilé.
Puis vient l'exécution des instructions après le ELSE. Elles utilisent le masque empilé au sommet de la pile, qui correspond à celui à l'entrée du IF.
Puis vient le moment d'exécuter les instructions après le IF : pas de masque, on exécute sur tout le vecteur.
===Les compteurs d'activité===
Une variante de la technique précédente remplace la pile de masques par des '''compteurs d'activité'''. La technique est similaire, si ce n'est qu'elle utilise moins de circuits. Avant , on avait une pile de masques de même taille, dont les bits sont à 0 ou 1 suivant que la condition est remplie. La pile de masque ressemble donc à ceci :
{|class="wikitable"
|-
! masque 1
| 1 || 1 || 1 || 1
|-
! masque 2
| 0 || 1 || 1 || 1
|-
! masque 3
| 0 || 1 || 1 || 1
|-
! masque 4
| 0 || 0 || 0 || 1
|-
! masque 1
| colspan="4" | vide
|}
Une manière équivalente de représenter cette pile de masque est de compter combien de bits sont à 0 dans chaque colonne. Attention : j'ai bien dit à 0 ! On obtient alors :
{|class="wikitable"
|-
! masque 1
| 3 || 1 || 1 || 0
|}
Et c'est le principe caché derrière la technique des compteurs d'activité. Chaque élément dans un vecteur, chaque place, se voit attribuer un compteur. Un compteur non-nul indique qu'il ne faut pas prendre en compte l’élément. Ce n'est qu'une fois que le compteur est nul que l'on effectue des opérations sur l’élément associé du vecteur.
À chaque fois qu'on entre dans une structure de contrôle, on teste une condition sur chaque élément. Si la condition est respectée pour un élément, alors le compteur ne change pas. Mais si la condition n'est pas respectée, alors on incrémente le compteur associé. En sortant de la structure de contrôle, on décrémente le compteur associé. Notons que les compteurs qui n'ont pas été incrémentés en entrant dans la structure de contrôle ne sont pas décrémentés en sortant. En clair, là où on empilait/dépilait un masque, on se contente d'incrémenter/décrémenter un compteur.
Utiliser un compteur en lieu et place d'une colonne entière dans la pile de masque utilise moins de bits. Et c'est sans doute pour cette raison que certaines cartes graphiques, comme les cartes graphiques intégrées d'Intel depuis 2004, utilisent cette technique.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes accélératrices 3D
| prevText=Les cartes accélératrices 3D
| next=La microarchitecture des processeurs de shaders
| nextText=La microarchitecture des processeurs de shaders
}}
{{autocat}}
q8ucm3q2po0e1ypjb89l5yo82wpxxy7
763433
763432
2026-04-10T23:51:42Z
Mewtow
31375
/* Les registres d'interface avec le pipeline graphique */
763433
wikitext
text/x-wiki
Les '''''shaders''''' sont des programmes informatiques exécutés par la carte graphique, et plus précisément par des processeurs de ''shaders''. Un point très important à comprendre est que chaque triangle ou pixel d'une scène 3D peut être traité indépendamment des autres. Le tout se résume comme suit :
: '''L’exécution d'un shader génère un grand nombre d'instances de ce shader, chacune traitant un paquet de pixels/sommets différent.'''
En conséquence, il est possible de traiter chaque instance d'un ''shader'' en parallèle des autres, en même temps, au lieu de traiter les instances l'une après l'autre.
La conséquence est que les cartes graphiques sont des architectures massivement parallèles, à savoir qu'elles sont capables d'effectuer un grand nombre de calculs indépendants en même temps. De plus, le parallélisme utilisé est du parallélisme de données, à savoir qu'on exécute le même programme sur des données différentes, chaque donnée étant traitée en parallèle des autres. Les cartes graphiques récentes incorporent toutes les techniques de parallélisme de donnée au niveau matériel, et nous allons toutes les détailler dans ce chapitre. S'il fallait résumer, elles ont plusieurs processeurs/cœurs, chaque cœur est capable d’exécuter des instructions SIMD (ils ne font que cela, à vrai dire), les cœurs sont fortement multithreadés, et j'en passe.
[[File:CPU and GPU.png|vignette|Comparaison du nombre de processeurs et de cœurs entre CPU et GPU.]]
Le premier point est qu'une carte graphique contient de nombreux processeurs, qui eux-mêmes contiennent plusieurs unités de calcul. Savoir combien de cœurs contient une carte graphique est cependant très compliqué, car la terminologie utilisée par les fabricants de carte graphique est particulièrement confuse. Il n'est pas rare que ceux-ci appellent cœurs ou processeurs, ce qui correspond en réalité à une unité de calcul d'un processeur normal, sans doute histoire de gonfler les chiffres. Et on peut généraliser à la majorité de la terminologie utilisée par les fabricants, que ce soit pour les termes ''warps processor'', ou autre, qui ne sont pas aisés à interpréter.
L'architecture d'une carte graphique récente est illustrée ci-dessous. Rien de bien déroutant pour qui a déjà étudié les architectures à parallélisme de données, mais quelques rappels ou explications ne peuvent pas faire de mal. Le premier point est la présence d'un grand nombre de processeurs/cœurs, les rectangles en bleu/rouge. Chacun d'entre eux contient un grand nombre de circuits de calculs, avec des circuits de calcul simples mais nombreux en rouge, et une unité pour les calculs complexes (trigonométriques, racines carrées, autres) en rouge. Le tout est relié à une hiérarchie mémoire indiquée en vert, comprenant des mémoires locales en complément de la mémoire vidéo principale. Le tout est alimenté par une unité de répartition, le '''''Thread Execution Control Unit''''' en jaune, qui répartit les différentes instances du ''shader'' sur les différents processeurs. Elle est aussi appelée le '''processeur de commandes''', comme nous le verrons dans quelques chapitres. Nous utiliserons le terme processeur de commande dans ce qui suit.
[[File:NVIDIA GPU Accelerator Block Diagram.png|centre|vignette|upright=2.5|Ce schéma illustre l'architecture d'un GPU en utilisant la terminologie NVIDIA. Comme on le voit, la carte graphique contient plusieurs cœurs de processeur distincts. Chacun d'entre eux contient plusieurs unités de calcul généralistes, appelées processeurs de threads, qui s'occupent de calculs simples (en bleu). D'autres calculs plus complexes sont pris en charge par une unité de calcul spécialisée (en rouge). Ces cœurs sont alimentés en instructions par le processeur de commandes, ici appelé ''Thread Execution Control Unit'', qui répartit les différents shaders sur chaque cœur. Enfin, on voit que chaque cœur a accès à une mémoire locale dédiée, en plus d'une mémoire vidéo partagée entre tous les cœurs.]]
Les portions bleu, jaune et verte du schéma précédent méritent chacune un chapitre séparé. La hiérarchie mémoire en vert fera l'objet d'un chapitre ultérieur. Quant au répartiteur en jaune, il sera détaillé en profondeur dans le prochain chapitre. Dans ce chapitre, nous allons voir comment fonctionnent les processeurs de ''shaders'', la partie bleue. Nous allons voir que ceux-ci ne sont pas très différents des processeurs que l'on trouve dans les ordinateurs normaux, du moins dans les grandes lignes. Ce sont des processeurs séquentiels, qui exécutent des instructions les unes après les autres. Ils ont des instructions machines, des modes d'adressage, un assembleur, des registres et tout ce qui fait qu'un processeur est un processeur. Néanmoins, il y a une différence de taille : ce sont des processeurs adaptés pour effectuer un grand nombre de calculs en parallèle.
==Les registres des processeurs de shaders==
Un processeur de shaders contient beaucoup de registres, sans quoi il ne pourrait pas faire son travail efficacement. Les plus intuitifs sont les '''registres généraux''', aussi appelés registres temporaires, qui servent à mémoriser n'importe quelle donnée utilisable par le processeur. Ils servent un peu à tout, le processeur peut les manipuler à loisir. Tout processeur digne de ce nom en possède. Mais un processeur de ''shader'' dispose aussi de registres spécialisés, qu'on ne trouve que sur les processeurs de ''shaders'', qui servent à l'interfacer avec le reste du pipeline graphique.
[[File:Architecture carte graphique vertex avec texture.PNG|centre|vignette|upright=2|Architecture carte graphique vertex avec texture]]
===Les registres d'interface avec le pipeline graphique===
Les processeurs de ''vertex shader'' reçoivent des 'sommets provenant de l'''input assembler'' et envoient leur résultat au rastériseur. Les processeurs de ''pixel shader'' reçoit des données provenant de l'unité de rastérisation; et envoie son résultat final aux ROPs. Et pour cela, le processeur de shader a des registres dédiés, qui servent d'interface avec le reste du pipeline graphique.
Les '''registres d'entrée''' se classent en deux sous-types : les registres d'attributs et les registres de constantes.
Les '''registres d'attributs''' réceptionnent soit les sommets provenant de l'''input assembler'', soit les pixels provenant de l'unité de rastérisation. Les registres d'entrée sont en lecture seule, du point de vue du processeur de shader, seule l'unité de rastérisation peut écrire dedans. Ils sont initialisés avant l'exécution de l'instance du ''shader''.
Les '''registres de sortie''' sont là où le processeur stocke les résultats à envoyer, soit au rastériseur pour un ''vertex shader'', soit aux ROP pour un ''pixel shader''. Les registres de sorties sont en écriture seule. Pour donner un exemple, les ''vertex shaders'' ont au minimum un registre pour la position du sommet dans l'espace (trois coordonnées), un autre pour la couleur/luminosité du sommet, un autre pour la couleur du brouillard, un autre pour les coordonnées de texture.
{|class="wikitable"
|+ Registres de sortie des ''pixel/vertex shaders''
|-
! Vertex shader
! Pixel shader
|-
| Couleur du pixel
| Couleur du sommet
|-
| Profondeur du pixel
| Position du sommet
|-
| rowspan="2" |
| Coordonnées de texture du sommet
|-
| Couleur de brouillard.
|}
===Les registres spécialisés internes===
D'autres registres spécialisés ne font pas l'interface avec le reste du GPU. Ils servent à stocker des constantes ou des données importantes, qui n'ont pas vraiment leur place dans les registres généraux.
Les '''registres de constantes''' servent pour stocker des constantes utiles pour le ''shader''. Par exemple, pour les ''vertex shaders'', ils stockent les matrices servant aux différentes étapes de transformation ou d'éclairage. Ces constantes sont placées dans ces registres peu après le chargement du vertex shader dans la mémoire vidéo. Toutefois, le vertex shader peut écrire dans ces registres, au prix d'une perte de performance particulièrement violente.
Les ''pixel/vertex shaders'' 1.0 ne géraient que des constantes flottantes pour les ''vertex shaders'', entières pour les ''pixel shaders''. Mais les ''pixel/vertex shaders'' 2.0 et 3.0 avaient des registres de constantes séparés pour les nombres entiers, les nombres flottants, et même les nombres booléens. Les constantes entières et booléennes étaient utilisées pour gérer les boucles, guère plus. Aussi, il y en avait 16, comparé aux centaines de registres de constantes flottants. Mais avec les ''pixel/vertex shaders'' 4.0 et plus, les registres de constante ont été fusionnés et n'ont plus de type prédéterminé, le programmeur gère ces registres comme il l'entend.
L'adressage des registres de constante est quelque peu particulier. Il faut dire qu'il y en a plusieurs milliers sur les processeurs de ''shaders'' modernes, au point qu'il serait plus juste de parler de mémoire RAM des constantes. Les registres de constante sont en effet un ''local store'' un peu spécial, intégré directement dans le processeur. Et le processeur accède à ce ''local store'' en utilisant une mode d'adressage semblable à celui utilisé pour la mémoire, avec un mode d'adressage indirect. L'adresse à lire dans ce ''local store'' est dans un registre, séparé du reste, appelé le '''registre d'adresse de constante'''.
Depuis les ''pixel/vertex shaders'' 3.0, les ''shaders'' sont capables d'effectuer des boucles et d'autres structures de contrôle familières pour les programmeurs. Et deux registres ont été intégrés afin d'améliorer les performances des structures de contrôle. Le premier est un registre à prédicat, qui sera vu dans la section sur le SIMD avec prédication. Le second est un '''registre compteur de boucle''', qui mémorise l'indice d'une boucle. Il est initialisé à 0, et est incrémenté à chaque fois qu'une boucle s'exécute.
Certains processeurs de shader ont aussi des '''registres de texture''' , qui servent d'interface avec la mémoire pour la gestion des textures. Ils mémorisent les texels lus par l'unité de texture. L'unité de texture lit un texel, plusieurs avec ''multitexturing'', et les place dans ces registres de texture. Les registres de texture sont parfois initialisés avant l'exécution du ''shader'', mais la plupart sont initialisé quand le ''shader'' termine une instruction de lecture de texture. Ils sont généralement en lecture seule, mais il y a des exceptions.
==Les processeurs de shaders modernes : les processeurs SIMD==
Maintenant, voyons quelles sont les instructions supportées par les processeurs de shaders modernes. Et si je dis moderne, c'est car nous ne parlerons que des GPU de l'époque DirectX 10 et après, pas des GPU de l'époque DirectX 9 et antérieur. La raison est que le jeu d'instruction des shaders a franchement évolué, avec le passage d'architectures VLIW à des architectures SIMD. Et cela a eu des conséquences assez profondes sur le jeu d'instruction et leur microarchitecture. Nous n'allons parler des GPU de type SIMD dans ce chapitre. Un chapitre dédié sera consacré aux GPU de type VLIW.
Le jeu d'instruction des GPU NVIDIA n'est pas encore connu à l'heure où j'écris ces lignes, la documentation du constructeur n'est pas disponible. Quelques chercheurs ont tenté de faire de la rétro-ingénierie du code de divers shaders pour retrouver le jeu d'instruction des divers GPU NVIDIA, ce qui fait qu'on a cependant une idée de ce dernier. Mais rien d'officiel. Par contre, AMD fournit librement cette documentation sur le net. Ce qui fait qu'on peut trouver des documents de ce genre :
* [https://developer.amd.com/wordpress/media/2012/12/AMD_Southern_Islands_Instruction_Set_Architecture.pdf Graphics Core Next 1 instruction set] ;
* [https://developer.amd.com/wordpress/media/2013/07/AMD_Sea_Islands_Instruction_Set_Architecture.pdf Graphics Core Next 2 instruction set] ;
* [https://developer.amd.com/wordpress/media/2013/12/AMD_GCN3_Instruction_Set_Architecture_rev1.1.pdf Graphics Core Next 3 and 4 instruction sets] ;
* [https://developer.amd.com/wp-content/resources/Vega_Shader_ISA_28July2017.pdf Graphics Core Next 5 instruction set] ;
* [https://developer.amd.com/wp-content/resources/Vega_7nm_Shader_ISA.pdf "Vega" 7nm instruction set architecture] (also referred to as Graphics Core Next 5.1) ;
* [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna3-shader-instruction-set-architecture-feb-2023_0.pdf Jeu d'instruction des GPU de type RDNA3 d'AMD].
===Les instructions SIMD===
Les '''instructions SIMD''' manipulent plusieurs nombres en même temps. Elles manipulent plus précisément des '''vecteurs''', des ensembles de plusieurs nombres entiers ou nombres flottants placés les uns à côté des autres, le tout ayant une taille fixe, qui sont stockés dans des registres spécialisés. En général, tous les vecteurs ont une taille fixe, peu importe leur contenu. Cela implique que suivant la taille des données à manipuler, on pourra en placer plus ou moins dans un vecteur. Par exemple, un vecteur de 128 bits pourra contenir 4 entiers de 32 bits, 4 flottants 32 bits, ou 8 entiers de 16 bits.
[[File:Vector register.png|centre|vignette|upright=2|Contenu d'un vecteur en fonction du type de données utilisé.]]
Les vecteurs sont stockés dans des '''registres vectoriels''', aussi appelés '''registres SIMD'''. Un registre vectoriel peut contenir un vecteur complet, pas plus. En conséquence, ils ont une taille assez importante : ils font généralement 128, 256, voire 512 bits, comparé aux 32/64 bits des registres des CPU. Les cartes graphiques modernes contiennent un très grand nombre de registres SIMD.
{|
|+ Comparaison entre un processeur sans registres vectoriels, et avec registres vectoriels.
|[[File:Non-SIMD cpu diagram1.svg|vignette|upright=1.5|CPU Non-SIMD]]
|[[File:SIMD cpu diagram1.svg|vignette|upright=1.5|CPU SIMD]]
|}
Une instruction SIMD traite chaque donnée du vecteur indépendamment des autres. Par exemple, une instruction d'addition vectorielle va additionner ensemble les données qui sont à la même place dans deux vecteurs, et placer le résultat dans un autre vecteur, à la même place.
[[File:Instructions SIMD.png|centre|vignette|upright=2.0|Instructions SIMD]]
Sur les cartes graphiques modernes, les vecteurs sont généralement des vecteurs qui regroupent plusieurs nombres flottants. De plus, les flottants en question sont des flottants dits simple précision, codés sur 32 bits. Mais il y a quelques exceptions, comme [https://www.realworldtech.com/apple-custom-gpu/ certains GPU d'Apple, qui ne gèrent majoritairement que des flottants codés sur 16 bits], avec des fonctionnalités pour la simple précision. Les anciennes cartes graphiques ne géraient pas du tout de vecteurs contenant des nombres entiers.
===Les instruction scalaires entières, typiques des CPU===
Un processeur SIMD gère donc des instructions SIMD, et les anciennes cartes graphiques ne disposaient que d'instructions de ce type. Mais depuis au moins une décennie, les processeurs de shaders gèrent des instructions normales, non-SIMD. De telles instructions sont appelées des '''instruction scalaires'''. En clair, il s'agit des instructions qu'on retrouve normalement tous les processeurs principaux (les CPU).
Il s'agit généralement d''''instructions entières''', agissent sur des registres entiers non-SIMD. Elles ne traitent pas de vecteur, mais de simples nombres entiers indépendants, sans regroupement d'aucune sorte. Typiquement, il s'agit d'opérations d'addition, de soustraction, des opérations logiques, des comparaisons, guère plus. On trouve aussi des opérations un peu originales, comme des calculs de valeur absolue, du minimum/maximum de deux opérandes, des opérations à prédicat comme une instruction CMOV, etc. Les cartes graphiques supportent rarement la multiplication, mais les plus récentes supportent des multiplications sur des opérandes de 16/32 bits. Par contre, aucune ne gère de division entière.
Les GPU modernes gèrent aussi des instructions de test et de branchement, là encore sur des nombres entiers. Les instructions de test et branchement sont généralement considérées comme à part des instructions de calcul, mais ce sont des opérations scalaires. Les comparaisons se font entre deux entiers scalaires, pas entre deux vecteurs. Retenez bien ce détail, car il sera très important pour la suite.
Les GPU modernes gèrent aussi des '''instructions flottantes scalaires''', à savoir que des instructions qui ont pour opérandes des nombres flottants isolés, qui ne sont pas dans un vecteur. Les processeurs principaux (CPU) d'un ordinateur sont capables de faire beaucoup de calculs arithmétiques simples sur des nombres flottants, comme des additions, des multiplications, des opérations bit-à-bit, éventuellement des divisions, etc. Il en est de même sur les GPUS. Mais ces derniers gèrent aussi de nombreuses instructions flottantes que les CPU n'incorporent presque pas.
Il est rare que les CPU soient capables de faire des opérations flottantes complexes, comme des calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse, etc. De tels calculs sont rares dans les programmes exécutables, alors que les calculs arithmétiques simples y sont légion. Mais le rendu 3D demande pas mal de calculs trigonométriques, de produits scalaires ou d'autres opérations. Par exemple, dans les chapitres précédents, nous avions abordé les calculs d'éclairage et avions vu qu'ils font beaucoup de calculs vectoriels avec des vecteurs comme la normale d'un sommet. Et ces calculs demandent de calculer des produits scalaires et vectoriels, qui eux-mêmes demandent des calculs trigonométriques comme le cosinus ou le sinus.
Aussi, les processeurs de ''shaders'' disposent souvent d'instructions flottantes spécialisées dans les calculs complexes : exponentielle/logarithme, racine carrée, racine carrée inverse, autres. Nous appellerons ces instructions des '''instructions transcendantales''', car elles effectuent des calculs de ce type.
Il faut noter que le processeur incorpore des registres dédiés aux scalaires, séparés des registres SIMD. Par séparés, on veut dire que ce sont des registres différents, adressés différemment, mais qu'ils sont aussi physiquement séparés dans le processeur, ils sont des bancs de registres différents.
===Les instructions en ''co-issue''===
Beaucoup de cartes graphiques récentes comme anciennes incorporent des '''instructions de ''co-issue''''' qui ne se trouvent que sur les cartes graphiques et n'ont aucun équivalent sur les CPUs. Les instructions de ''co-issue'' regroupent plusieurs opérations par instruction. Par exemple, elles peuvent combiner une opération vectorielle avec une opération scalaire. Ou encore, elles peuvent regrouper une opération scalaire, une opération vectorielle et un branchement. Il s'agit d'instructions qui ressemblent grandement à ce qu'on trouve sur les processeurs VLIW.
Un point important est que les cartes graphiques modernes disposent d'instructions à ''co-issue'' en plus des instructions normales. Les instructions à ''co-issue'' sont complémentaire des instructions normales, elles ne les remplacent pas. Les deux peuvent s'utiliser en même temps, dans un même shader. Il a cependant existé des cartes graphiques assez anciennes sur lesquelles toutes les instructions étaient des instructions à ''co-issue'' : certains processeurs de shaders VLIW anciens sont de ce type.
Il y a de nombreuses contraintes quant au regroupement des deux opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre. L'exemple type de ''co-issue'' est la ''co-issue'' entre opérations scalaires et vectorielles : il n'est pas possible de regrouper deux instructions scalaires ou deux instructions vectorielles. La seule possibilité est de regrouper une opération scalaire et une opération vectorielle. La raison à cela est qu'opérations scalaires et vectorielles sont calculées dans des circuits séparés : le processeur incorpore une unité de calcul scalaire et une unité de calcul SIMD, et peut utiliser les deux en parallèle, en même temps. Mais nous verrons cela dans quelques chapitres.
Pour simplifier, cette technique permettait d’exécuter deux opérations arithmétiques en même temps, en parallèle : une opération vectorielle appliquée aux couleurs R, G, et B, et une opération scalaire appliquée à la couleur de transparence. Si cela semble intéressant sur le papier, cela complexifie fortement le processeur de shader, ainsi que la traduction à la volée des shaders en instructions machine.
===Un exemple : le jeu d’instruction du GPU de la Geforce 3===
La première carte graphique commerciale grand public à disposer d'une unité de vertex programmable est la Geforce 3. Celui-ci respectait le format de vertex shader 1.1. L'ensemble des informations à savoir sur cette unité est disponible dans l'article [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p149-lindholm.pdf "A user programmable vertex engine"], disponible sur le net. . Le processeur de cette carte était capable de gérer un seul type de données : les nombres flottants de norme IEEE754. Toutes les informations concernant la coordonnée d'une vertice, voire ses différentes couleurs, doivent être encodées en utilisant ces flottants.
Les processeurs de vertices de la Geforce 3 disposent de registres registres SIMD qui font 128 bits, soit 4 flottants de 32 bits. Elle contient 16 registres d'entrée, 16 registres de sortie, 32 registres généraux. La mémoire des constantes contient 512 "registres".
Le processeur de la Geforce 3 est capable d’exécuter 17 instructions différentes, dont voici les principales :
{|class="wikitable"
|-
!OpCode!!Nom!!Description
|-
! colspan="3" | Opérations mémoire
|-
|MOV||Move||vector -> vector
|-
|ARL||Address register load||miscellaneous
|-
! colspan="3" | Opérations arithmétiques
|-
|ADD||Add||vector -> vector
|-
|MUL||Multiply||vector -> vector
|-
|MAD||Multiply and add||vector -> vector
|-
|MIN||Minimum||vector -> vector
|-
|MAX||Maximum||vector -> vector
|-
|SLT||Set on less than||vector -> vector
|-
|SGE||Set on greater or equal||vector -> vector
|-
|LOG||Log base 2||miscellaneous
|-
|EXP||Exp base 2||miscellaneous
|-
|RCP||Reciprocal||scalar-> replicated scalar
|-
|RSQ||Reciprocal square root||scalar-> replicated scalar
|-
! colspan="3" | Opérations trigonométriques
|-
|DP3||3 term dot product||vector-> replicated scalar
|-
|DP4||4 term dot product||vector-> replicated scalar
|-
|DST||Distance||vector -> vector
|-
! colspan="3" | Opérations d'éclairage géométrique
|-
|LIT||Phong lighting||Calcule l'éclairage de Gouraud
|}
L'instruction la plus intéressante est clairement la dernière : elle éclaire un sommet, en utilisant un éclairage de Phong. Les autres instructions permettent d'implémenter un autre algorithme si besoin, mais cette forme d'éclairage est déjà là à la base.
Les autres instructions sont surtout des instructions arithmétiques : multiplications, additions, exponentielles, logarithmes, racines carrées, etc. Pour les instructions d'accès à la mémoire, on trouve une instruction MOV qui déplace le contenu d'un registre dans un autre et une instruction de calcul d'adresse, mais aucune instruction d'accès à la mémoire sur le processeur de la Geforce 3. Plus tard, les unités de ''vertex shader'' ont acquis la possibilité de lire des données dans une texture.
On remarque que la division est absente. Il faut dire que la contrainte qui veut que toutes ces instructions s’exécutent en un cycle d'horloge pose quelques problèmes avec la division, qui est une opération plutôt lourde en hardware. À la place, on trouve l'instruction RCP, capable de calculer 1/x, avec x un flottant. Cela permet ainsi de simuler une division : pour obtenir Y/X, il suffit de calculer 1/X avec RCP, et de multiplier le résultat par Y.
==La prédication et le SIMT==
Les cartes graphiques récentes peuvent effectuer des branchements, mais ceux-ci sont tout sauf performants. Dès qu'un branchement survient, le processeur est obligé de traiter chaque élément du vecteur un par un, au lieu de tous les traiter en même temps en parallèle. Les performances s'en ressentent, ce qui fait que les branchements sont à éviter le plus possible. Pour améliorer la gestion des conditions, les cartes graphiques modernes incorporent des instructions spécialisées qui permettent de remplacer des codes remplis de branchements par des codes plus simples, compatibles avec l'organisation des données en vecteurs.
Si on met de côté le support de certaines instructions courantes, comme la valeur absolue, ou le calcul du minimum/maximum, la technique la plus importante est la technique dite de '''prédication'''. L'idée est que quand une instruction effectue un calcul sur un ou deux vecteurs, certains éléments du vecteur sont ignorés. Les éléments à ignorer sont choisis suivant le résultat d'une instruction de comparaison, qui effectue un test : les éléments pour lesquels ce test est respecté sont pris en compte, ceux qui ne passent pas le test sont ignorés.
Pour donner un exemple d'utilisation, imaginons que l'on ait un vecteur dans lequel on veut remplacer toutes les valeurs négatives par des 0. Dans ce cas, on utilise :
* une instruction de comparaison, qui compare chaque élément du vecteur avec 0 et génère plusieurs bits de résultat ;
* suivi d'une instruction à prédicat qui met à zéro les éléments pour lesquels les bits de résultat précédents sont à 1.
Elle est implémentée grâce à un registre appelé le '''''Vector Mask Register'''''. Celui-ci permet de stocker des informations qui permettront de sélectionner certaines données et pas d'autres pour faire notre calcul. Il est mis à jour par des instructions de comparaison. le ''Vector Mask Register'' stocke un bit pour chaque flottant présent dans le vecteur à traiter, bit qui indique s'il faut appliquer l'instruction sur ce flottant. Si ce bit est à 1, notre instruction doit s’exécuter sur la donnée associée à ce bit. Sinon, notre instruction ne doit pas la modifier. On peut ainsi traiter seulement une partie des registres stockant des vecteurs SIMD.
[[File:Vector mask register.png|centre|vignette|upright=2.0|''Vector mask register'']]
===La prédication avec une pile SIMT===
Au niveau du jeu d’instruction, les architectures SIMT implémentent de la prédication, sous une forme améliorée. Les processeurs SIMT actuels sont surtout utilisées sur les processeurs intégrés aux cartes graphiques. Et ces derniers gèrent très mal les branchements, et encore : beaucoup de cartes graphiques, même récentes, ne gèrent tout simplement pas les branchements. Elles doivent donc se débrouiller avec uniquement la prédication, là où les processeurs SIMD utilisent des branchements normaux en complément de la prédication. Insistons sur le fait que cet usage exclusif de la prédication n'est présent que sur une sous-partie des architectures SIMT, le seul exemple que l'auteur de ce wikilivre connait étant celui des cartes graphiques.
Les architectures SIMT sans branchements doivent donc trouver des solutions pour gérer les structures de contrôle imbriquées, à savoir une boucle placée à l'intérieur d'une autre boucle, un IF...ELSE dans un autre IF...ELSE, etc. Elles utilisent pour cela la prédication, combinée avec des mécanismes annexes. Le premier d'entre eux est l'usage de plusieurs registres de masques organisés d'une manière bien précise, l'autre est l'usage de compteurs d'activité. Voyons ces deux techniques.
La '''pile de masques''' remplace le ou les registres de masque. Sans elle, le processeur SIMD incorpore un registre de masque qui est adressé implicitement ou explicitement. Éventuellement, le processeur peut contenir plusieurs registres de masque séparés adressables via un nom de registre. Avec elle, le processeur SIMD incorpore plusieurs registres de masque organisé en pile. Le registre de masque est donc remplacé par une mémoire LIFO, une pile, dans laquelle plusieurs masques sont empilés.
Le tout forme une pile, similaire à la pile d'appel, sauf qu'elle est utilisée pour empiler des masques. Un masque est calculé et empilé à chaque entrée dans une structure de contrôle, puis dépilé une fois la structure de contrôle exécutée. L'empilement et le dépilement des masques est effectué par des instructions PUSH et POP, présentes dans le jeu d'instruction du processeur SIMD.
Le calcul des masques doit répondre à plusieurs impératifs.
* Premièrement, chaque masque se calcule en faisant un ET entre le masque précédent et le masque calculé par l'instruction de test. Cela permet de ne pas réveiller d’élément au beau milieu d'une structure imbriquée. Si in IF désactive certains éléments du vecteur, une condition imbriquée dans ce IF ne doit pas réveiller cet élément. Le fait de faire un ET entre les masques garantit cela.
* Deuxièmement, les masques doivent être empilés et dépilés correctement. Au moment de rentrer dans une structure de contrôle, on effectue une instruction de test associée à la structure de contrôle, qui calcule un masque, et on empile le masque calculé. Au moment de sortir de la structure de contrôle, on dépile le masque en question.
L'implémentation demande d'utiliser une mémoire LIFO pour stocker la pile de masques, et quelques circuits annexes. Il faut notamment un circuit relié à l'ALU qui récupère les conditions, les résultats des comparaisons, et qui effectue le ET pour combiner les masques.
Pour donner un exemple, prenons le code suivant, qui est volontairement simpliste et ne sert qu'à des fins d'explication :
<syntaxhighlight lang="c">
if ( condition 1 )
{
if ( condition 2 )
{
...
}
else
{
...
}
Autres instructions
}
Instructions après le IF...
</syntaxhighlight>
Imaginons que l'on traite des vecteurs de 8 éléments.
Pour le vecteur considéré, la première condition (a > 0) n'est respectée que par les 4 premiers éléments. L'instruction de condition calcule alors le masque correspondant : 1111 0000. Le masque est alors calculé, puis empilé au sommet de la pile.
La seconde instruction de test, qui teste la variable b, est maintenant valide pour les 4 bits du milieu du masque. Mais n'allez pas croire que le masque correspondant soit 0011 11100 : il faut tenir compte de la condition précédente, qui a éliminé les 4 derniers éléments. Pour cela, on fait un ET logique entre le masque précédent, et le masque calculé par la condition. Le masque au sommet de la pile est donc lu, combiné avec le masque calculé par l'instruction, ce qui donne le masque final. Le masque final est alors empilé au sommet de la pile.
On exécute alors l'instruction du IF, en tenant compte du masque qui est au sommet de la pile. Si le IF était plus compliqué, toutes les instructions suivantes tiendraient compte du masque. En fait, le masque est pris en compte tant qu'il n'est pas dépilé. Une fois que le IF est terminé, le masque est dépilé.
On passe alors au ELSE, et rebelotte. Le masque pour le ELSE est calculé en combinant le masque au sommet de la pile avec la condition du ELSE. Le masque au sommet de la pile est celui calculé à l'entrée du premier IF, pas le second qui a été dépilé. Les instructions du ELSE sont alors exécutées en tenant compte de ce masque. Une fois qu'elles sont toutes exécutées, le masque est dépilé.
Puis vient l'exécution des instructions après le ELSE. Elles utilisent le masque empilé au sommet de la pile, qui correspond à celui à l'entrée du IF.
Puis vient le moment d'exécuter les instructions après le IF : pas de masque, on exécute sur tout le vecteur.
===Les compteurs d'activité===
Une variante de la technique précédente remplace la pile de masques par des '''compteurs d'activité'''. La technique est similaire, si ce n'est qu'elle utilise moins de circuits. Avant , on avait une pile de masques de même taille, dont les bits sont à 0 ou 1 suivant que la condition est remplie. La pile de masque ressemble donc à ceci :
{|class="wikitable"
|-
! masque 1
| 1 || 1 || 1 || 1
|-
! masque 2
| 0 || 1 || 1 || 1
|-
! masque 3
| 0 || 1 || 1 || 1
|-
! masque 4
| 0 || 0 || 0 || 1
|-
! masque 1
| colspan="4" | vide
|}
Une manière équivalente de représenter cette pile de masque est de compter combien de bits sont à 0 dans chaque colonne. Attention : j'ai bien dit à 0 ! On obtient alors :
{|class="wikitable"
|-
! masque 1
| 3 || 1 || 1 || 0
|}
Et c'est le principe caché derrière la technique des compteurs d'activité. Chaque élément dans un vecteur, chaque place, se voit attribuer un compteur. Un compteur non-nul indique qu'il ne faut pas prendre en compte l’élément. Ce n'est qu'une fois que le compteur est nul que l'on effectue des opérations sur l’élément associé du vecteur.
À chaque fois qu'on entre dans une structure de contrôle, on teste une condition sur chaque élément. Si la condition est respectée pour un élément, alors le compteur ne change pas. Mais si la condition n'est pas respectée, alors on incrémente le compteur associé. En sortant de la structure de contrôle, on décrémente le compteur associé. Notons que les compteurs qui n'ont pas été incrémentés en entrant dans la structure de contrôle ne sont pas décrémentés en sortant. En clair, là où on empilait/dépilait un masque, on se contente d'incrémenter/décrémenter un compteur.
Utiliser un compteur en lieu et place d'une colonne entière dans la pile de masque utilise moins de bits. Et c'est sans doute pour cette raison que certaines cartes graphiques, comme les cartes graphiques intégrées d'Intel depuis 2004, utilisent cette technique.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes accélératrices 3D
| prevText=Les cartes accélératrices 3D
| next=La microarchitecture des processeurs de shaders
| nextText=La microarchitecture des processeurs de shaders
}}
{{autocat}}
ssgb8bav1eblalf5lbevcxfgquxtwho
763434
763433
2026-04-10T23:56:22Z
Mewtow
31375
/* Les registres des processeurs de shaders */
763434
wikitext
text/x-wiki
Les '''''shaders''''' sont des programmes informatiques exécutés par la carte graphique, et plus précisément par des processeurs de ''shaders''. Un point très important à comprendre est que chaque triangle ou pixel d'une scène 3D peut être traité indépendamment des autres. Le tout se résume comme suit :
: '''L’exécution d'un shader génère un grand nombre d'instances de ce shader, chacune traitant un paquet de pixels/sommets différent.'''
En conséquence, il est possible de traiter chaque instance d'un ''shader'' en parallèle des autres, en même temps, au lieu de traiter les instances l'une après l'autre.
La conséquence est que les cartes graphiques sont des architectures massivement parallèles, à savoir qu'elles sont capables d'effectuer un grand nombre de calculs indépendants en même temps. De plus, le parallélisme utilisé est du parallélisme de données, à savoir qu'on exécute le même programme sur des données différentes, chaque donnée étant traitée en parallèle des autres. Les cartes graphiques récentes incorporent toutes les techniques de parallélisme de donnée au niveau matériel, et nous allons toutes les détailler dans ce chapitre. S'il fallait résumer, elles ont plusieurs processeurs/cœurs, chaque cœur est capable d’exécuter des instructions SIMD (ils ne font que cela, à vrai dire), les cœurs sont fortement multithreadés, et j'en passe.
[[File:CPU and GPU.png|vignette|Comparaison du nombre de processeurs et de cœurs entre CPU et GPU.]]
Le premier point est qu'une carte graphique contient de nombreux processeurs, qui eux-mêmes contiennent plusieurs unités de calcul. Savoir combien de cœurs contient une carte graphique est cependant très compliqué, car la terminologie utilisée par les fabricants de carte graphique est particulièrement confuse. Il n'est pas rare que ceux-ci appellent cœurs ou processeurs, ce qui correspond en réalité à une unité de calcul d'un processeur normal, sans doute histoire de gonfler les chiffres. Et on peut généraliser à la majorité de la terminologie utilisée par les fabricants, que ce soit pour les termes ''warps processor'', ou autre, qui ne sont pas aisés à interpréter.
L'architecture d'une carte graphique récente est illustrée ci-dessous. Rien de bien déroutant pour qui a déjà étudié les architectures à parallélisme de données, mais quelques rappels ou explications ne peuvent pas faire de mal. Le premier point est la présence d'un grand nombre de processeurs/cœurs, les rectangles en bleu/rouge. Chacun d'entre eux contient un grand nombre de circuits de calculs, avec des circuits de calcul simples mais nombreux en rouge, et une unité pour les calculs complexes (trigonométriques, racines carrées, autres) en rouge. Le tout est relié à une hiérarchie mémoire indiquée en vert, comprenant des mémoires locales en complément de la mémoire vidéo principale. Le tout est alimenté par une unité de répartition, le '''''Thread Execution Control Unit''''' en jaune, qui répartit les différentes instances du ''shader'' sur les différents processeurs. Elle est aussi appelée le '''processeur de commandes''', comme nous le verrons dans quelques chapitres. Nous utiliserons le terme processeur de commande dans ce qui suit.
[[File:NVIDIA GPU Accelerator Block Diagram.png|centre|vignette|upright=2.5|Ce schéma illustre l'architecture d'un GPU en utilisant la terminologie NVIDIA. Comme on le voit, la carte graphique contient plusieurs cœurs de processeur distincts. Chacun d'entre eux contient plusieurs unités de calcul généralistes, appelées processeurs de threads, qui s'occupent de calculs simples (en bleu). D'autres calculs plus complexes sont pris en charge par une unité de calcul spécialisée (en rouge). Ces cœurs sont alimentés en instructions par le processeur de commandes, ici appelé ''Thread Execution Control Unit'', qui répartit les différents shaders sur chaque cœur. Enfin, on voit que chaque cœur a accès à une mémoire locale dédiée, en plus d'une mémoire vidéo partagée entre tous les cœurs.]]
Les portions bleu, jaune et verte du schéma précédent méritent chacune un chapitre séparé. La hiérarchie mémoire en vert fera l'objet d'un chapitre ultérieur. Quant au répartiteur en jaune, il sera détaillé en profondeur dans le prochain chapitre. Dans ce chapitre, nous allons voir comment fonctionnent les processeurs de ''shaders'', la partie bleue. Nous allons voir que ceux-ci ne sont pas très différents des processeurs que l'on trouve dans les ordinateurs normaux, du moins dans les grandes lignes. Ce sont des processeurs séquentiels, qui exécutent des instructions les unes après les autres. Ils ont des instructions machines, des modes d'adressage, un assembleur, des registres et tout ce qui fait qu'un processeur est un processeur. Néanmoins, il y a une différence de taille : ce sont des processeurs adaptés pour effectuer un grand nombre de calculs en parallèle.
==Les registres des processeurs de shaders==
Un processeur de shaders contient beaucoup de registres, sans quoi il ne pourrait pas faire son travail efficacement. Les plus intuitifs sont les '''registres généraux''', aussi appelés registres temporaires, qui servent à mémoriser n'importe quelle donnée utilisable par le processeur. Ils servent un peu à tout, le processeur peut les manipuler à loisir. Tout processeur digne de ce nom en possède. Mais un processeur de ''shader'' dispose aussi de registres spécialisés, qu'on ne trouve que sur les processeurs de ''shaders'', qui servent à l'interfacer avec le reste du pipeline graphique.
[[File:Architecture carte graphique vertex avec texture.PNG|centre|vignette|upright=2|Architecture carte graphique vertex avec texture]]
===Les registres d'interface avec le pipeline graphique===
Les processeurs de ''vertex shader'' reçoivent des 'sommets provenant de l'''input assembler'' et envoient leur résultat au rastériseur. Les processeurs de ''pixel shader'' reçoit des données provenant de l'unité de rastérisation; et envoie son résultat final aux ROPs. Et pour cela, le processeur de shader a des registres dédiés, qui servent d'interface avec le reste du pipeline graphique.
Les '''registres de sortie''' sont là où le processeur stocke les résultats à envoyer, soit au rastériseur pour un ''vertex shader'', soit aux ROP pour un ''pixel shader''. Les registres de sorties sont en écriture seule. Pour donner un exemple, les ''vertex shaders'' ont au minimum un registre pour la position du sommet dans l'espace (trois coordonnées), un autre pour la couleur/luminosité du sommet, un autre pour la couleur du brouillard, un autre pour les coordonnées de texture.
{|class="wikitable"
|+ Registres de sortie des ''pixel/vertex shaders''
|-
! Vertex shader
! Pixel shader
|-
| Couleur du pixel
| Couleur du sommet
|-
| Profondeur du pixel
| Position du sommet
|-
| rowspan="2" |
| Coordonnées de texture du sommet
|-
| Couleur de brouillard.
|}
Les '''registres d'entrée''' se classent en deux sous-types : les registres d'attributs et les registres de constantes. Ils servent à mémoriser des informations différentes. Les premiers mémorisent des sommets/pixels, alors que les seconds mémorisent des données comme les matrices de transformation, les adresses de texture, et bien d'autres. Les premières varient d'une instance de shader à l'autre, alors que les secondes sont constantes pour un objet/modèle 3D (pour un ''draw call'' , pour être plus précis).
Les '''registres d'attributs''' réceptionnent soit les sommets provenant de l'''input assembler'', soit les pixels provenant de l'unité de rastérisation. Les registres d'entrée sont en lecture seule, du point de vue du processeur de shader, seule l'unité de rastérisation peut écrire dedans. Ils sont initialisés avant l'exécution de l'instance du ''shader''.
Les '''registres de constantes''' mémorisent des constantes utiles pour le ''shader''. Par exemple, pour les ''vertex shaders'', ils stockent les matrices servant aux différentes étapes de transformation ou d'éclairage. Les ''pixel/vertex shaders'' 1.0 ne géraient que des constantes flottantes pour les ''vertex shaders'', entières pour les ''pixel shaders''. Mais les ''pixel/vertex shaders'' 2.0 et 3.0 avaient des registres de constantes séparés pour les nombres entiers, les nombres flottants, et même les nombres booléens. Les constantes entières et booléennes étaient utilisées pour gérer les boucles, guère plus. Aussi, il y en avait 16, comparé aux centaines de registres de constantes flottants. Mais avec les ''pixel/vertex shaders'' 4.0 et plus, les registres de constante ont été fusionnés et n'ont plus de type prédéterminé, le programmeur gère ces registres comme il l'entend.
L'adressage des registres de constante est quelque peu particulier. Il faut dire qu'il y en a plusieurs milliers sur les processeurs de ''shaders'' modernes, au point qu'il serait plus juste de parler de mémoire RAM des constantes. Les registres de constante sont en effet un ''local store'' un peu spécial, intégré directement dans le processeur. Et le processeur accède à ce ''local store'' en utilisant une mode d'adressage semblable à celui utilisé pour la mémoire, avec un mode d'adressage indirect. L'adresse à lire dans ce ''local store'' est dans un registre, séparé du reste, appelé le '''registre d'adresse de constante'''.
===Les registres spécialisés internes===
D'autres registres spécialisés ne font pas l'interface avec le reste du GPU. Ils servent à stocker des constantes ou des données importantes, qui n'ont pas vraiment leur place dans les registres généraux.
Depuis les ''pixel/vertex shaders'' 3.0, les ''shaders'' sont capables d'effectuer des boucles et d'autres structures de contrôle familières pour les programmeurs. Et deux registres ont été intégrés afin d'améliorer les performances des structures de contrôle. Le premier est un registre à prédicat, qui sera vu dans la section sur le SIMD avec prédication. Le second est un '''registre compteur de boucle''', qui mémorise l'indice d'une boucle. Il est initialisé à 0, et est incrémenté à chaque fois qu'une boucle s'exécute.
Certains processeurs de shader ont aussi des '''registres de texture''' , qui servent d'interface avec la mémoire pour la gestion des textures. Ils mémorisent les texels lus par l'unité de texture. L'unité de texture lit un texel, plusieurs avec ''multitexturing'', et les place dans ces registres de texture. Les registres de texture sont parfois initialisés avant l'exécution du ''shader'', mais la plupart sont initialisé quand le ''shader'' termine une instruction de lecture de texture. Ils sont généralement en lecture seule, mais il y a des exceptions.
==Les processeurs de shaders modernes : les processeurs SIMD==
Maintenant, voyons quelles sont les instructions supportées par les processeurs de shaders modernes. Et si je dis moderne, c'est car nous ne parlerons que des GPU de l'époque DirectX 10 et après, pas des GPU de l'époque DirectX 9 et antérieur. La raison est que le jeu d'instruction des shaders a franchement évolué, avec le passage d'architectures VLIW à des architectures SIMD. Et cela a eu des conséquences assez profondes sur le jeu d'instruction et leur microarchitecture. Nous n'allons parler des GPU de type SIMD dans ce chapitre. Un chapitre dédié sera consacré aux GPU de type VLIW.
Le jeu d'instruction des GPU NVIDIA n'est pas encore connu à l'heure où j'écris ces lignes, la documentation du constructeur n'est pas disponible. Quelques chercheurs ont tenté de faire de la rétro-ingénierie du code de divers shaders pour retrouver le jeu d'instruction des divers GPU NVIDIA, ce qui fait qu'on a cependant une idée de ce dernier. Mais rien d'officiel. Par contre, AMD fournit librement cette documentation sur le net. Ce qui fait qu'on peut trouver des documents de ce genre :
* [https://developer.amd.com/wordpress/media/2012/12/AMD_Southern_Islands_Instruction_Set_Architecture.pdf Graphics Core Next 1 instruction set] ;
* [https://developer.amd.com/wordpress/media/2013/07/AMD_Sea_Islands_Instruction_Set_Architecture.pdf Graphics Core Next 2 instruction set] ;
* [https://developer.amd.com/wordpress/media/2013/12/AMD_GCN3_Instruction_Set_Architecture_rev1.1.pdf Graphics Core Next 3 and 4 instruction sets] ;
* [https://developer.amd.com/wp-content/resources/Vega_Shader_ISA_28July2017.pdf Graphics Core Next 5 instruction set] ;
* [https://developer.amd.com/wp-content/resources/Vega_7nm_Shader_ISA.pdf "Vega" 7nm instruction set architecture] (also referred to as Graphics Core Next 5.1) ;
* [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna3-shader-instruction-set-architecture-feb-2023_0.pdf Jeu d'instruction des GPU de type RDNA3 d'AMD].
===Les instructions SIMD===
Les '''instructions SIMD''' manipulent plusieurs nombres en même temps. Elles manipulent plus précisément des '''vecteurs''', des ensembles de plusieurs nombres entiers ou nombres flottants placés les uns à côté des autres, le tout ayant une taille fixe, qui sont stockés dans des registres spécialisés. En général, tous les vecteurs ont une taille fixe, peu importe leur contenu. Cela implique que suivant la taille des données à manipuler, on pourra en placer plus ou moins dans un vecteur. Par exemple, un vecteur de 128 bits pourra contenir 4 entiers de 32 bits, 4 flottants 32 bits, ou 8 entiers de 16 bits.
[[File:Vector register.png|centre|vignette|upright=2|Contenu d'un vecteur en fonction du type de données utilisé.]]
Les vecteurs sont stockés dans des '''registres vectoriels''', aussi appelés '''registres SIMD'''. Un registre vectoriel peut contenir un vecteur complet, pas plus. En conséquence, ils ont une taille assez importante : ils font généralement 128, 256, voire 512 bits, comparé aux 32/64 bits des registres des CPU. Les cartes graphiques modernes contiennent un très grand nombre de registres SIMD.
{|
|+ Comparaison entre un processeur sans registres vectoriels, et avec registres vectoriels.
|[[File:Non-SIMD cpu diagram1.svg|vignette|upright=1.5|CPU Non-SIMD]]
|[[File:SIMD cpu diagram1.svg|vignette|upright=1.5|CPU SIMD]]
|}
Une instruction SIMD traite chaque donnée du vecteur indépendamment des autres. Par exemple, une instruction d'addition vectorielle va additionner ensemble les données qui sont à la même place dans deux vecteurs, et placer le résultat dans un autre vecteur, à la même place.
[[File:Instructions SIMD.png|centre|vignette|upright=2.0|Instructions SIMD]]
Sur les cartes graphiques modernes, les vecteurs sont généralement des vecteurs qui regroupent plusieurs nombres flottants. De plus, les flottants en question sont des flottants dits simple précision, codés sur 32 bits. Mais il y a quelques exceptions, comme [https://www.realworldtech.com/apple-custom-gpu/ certains GPU d'Apple, qui ne gèrent majoritairement que des flottants codés sur 16 bits], avec des fonctionnalités pour la simple précision. Les anciennes cartes graphiques ne géraient pas du tout de vecteurs contenant des nombres entiers.
===Les instruction scalaires entières, typiques des CPU===
Un processeur SIMD gère donc des instructions SIMD, et les anciennes cartes graphiques ne disposaient que d'instructions de ce type. Mais depuis au moins une décennie, les processeurs de shaders gèrent des instructions normales, non-SIMD. De telles instructions sont appelées des '''instruction scalaires'''. En clair, il s'agit des instructions qu'on retrouve normalement tous les processeurs principaux (les CPU).
Il s'agit généralement d''''instructions entières''', agissent sur des registres entiers non-SIMD. Elles ne traitent pas de vecteur, mais de simples nombres entiers indépendants, sans regroupement d'aucune sorte. Typiquement, il s'agit d'opérations d'addition, de soustraction, des opérations logiques, des comparaisons, guère plus. On trouve aussi des opérations un peu originales, comme des calculs de valeur absolue, du minimum/maximum de deux opérandes, des opérations à prédicat comme une instruction CMOV, etc. Les cartes graphiques supportent rarement la multiplication, mais les plus récentes supportent des multiplications sur des opérandes de 16/32 bits. Par contre, aucune ne gère de division entière.
Les GPU modernes gèrent aussi des instructions de test et de branchement, là encore sur des nombres entiers. Les instructions de test et branchement sont généralement considérées comme à part des instructions de calcul, mais ce sont des opérations scalaires. Les comparaisons se font entre deux entiers scalaires, pas entre deux vecteurs. Retenez bien ce détail, car il sera très important pour la suite.
Les GPU modernes gèrent aussi des '''instructions flottantes scalaires''', à savoir que des instructions qui ont pour opérandes des nombres flottants isolés, qui ne sont pas dans un vecteur. Les processeurs principaux (CPU) d'un ordinateur sont capables de faire beaucoup de calculs arithmétiques simples sur des nombres flottants, comme des additions, des multiplications, des opérations bit-à-bit, éventuellement des divisions, etc. Il en est de même sur les GPUS. Mais ces derniers gèrent aussi de nombreuses instructions flottantes que les CPU n'incorporent presque pas.
Il est rare que les CPU soient capables de faire des opérations flottantes complexes, comme des calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse, etc. De tels calculs sont rares dans les programmes exécutables, alors que les calculs arithmétiques simples y sont légion. Mais le rendu 3D demande pas mal de calculs trigonométriques, de produits scalaires ou d'autres opérations. Par exemple, dans les chapitres précédents, nous avions abordé les calculs d'éclairage et avions vu qu'ils font beaucoup de calculs vectoriels avec des vecteurs comme la normale d'un sommet. Et ces calculs demandent de calculer des produits scalaires et vectoriels, qui eux-mêmes demandent des calculs trigonométriques comme le cosinus ou le sinus.
Aussi, les processeurs de ''shaders'' disposent souvent d'instructions flottantes spécialisées dans les calculs complexes : exponentielle/logarithme, racine carrée, racine carrée inverse, autres. Nous appellerons ces instructions des '''instructions transcendantales''', car elles effectuent des calculs de ce type.
Il faut noter que le processeur incorpore des registres dédiés aux scalaires, séparés des registres SIMD. Par séparés, on veut dire que ce sont des registres différents, adressés différemment, mais qu'ils sont aussi physiquement séparés dans le processeur, ils sont des bancs de registres différents.
===Les instructions en ''co-issue''===
Beaucoup de cartes graphiques récentes comme anciennes incorporent des '''instructions de ''co-issue''''' qui ne se trouvent que sur les cartes graphiques et n'ont aucun équivalent sur les CPUs. Les instructions de ''co-issue'' regroupent plusieurs opérations par instruction. Par exemple, elles peuvent combiner une opération vectorielle avec une opération scalaire. Ou encore, elles peuvent regrouper une opération scalaire, une opération vectorielle et un branchement. Il s'agit d'instructions qui ressemblent grandement à ce qu'on trouve sur les processeurs VLIW.
Un point important est que les cartes graphiques modernes disposent d'instructions à ''co-issue'' en plus des instructions normales. Les instructions à ''co-issue'' sont complémentaire des instructions normales, elles ne les remplacent pas. Les deux peuvent s'utiliser en même temps, dans un même shader. Il a cependant existé des cartes graphiques assez anciennes sur lesquelles toutes les instructions étaient des instructions à ''co-issue'' : certains processeurs de shaders VLIW anciens sont de ce type.
Il y a de nombreuses contraintes quant au regroupement des deux opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre. L'exemple type de ''co-issue'' est la ''co-issue'' entre opérations scalaires et vectorielles : il n'est pas possible de regrouper deux instructions scalaires ou deux instructions vectorielles. La seule possibilité est de regrouper une opération scalaire et une opération vectorielle. La raison à cela est qu'opérations scalaires et vectorielles sont calculées dans des circuits séparés : le processeur incorpore une unité de calcul scalaire et une unité de calcul SIMD, et peut utiliser les deux en parallèle, en même temps. Mais nous verrons cela dans quelques chapitres.
Pour simplifier, cette technique permettait d’exécuter deux opérations arithmétiques en même temps, en parallèle : une opération vectorielle appliquée aux couleurs R, G, et B, et une opération scalaire appliquée à la couleur de transparence. Si cela semble intéressant sur le papier, cela complexifie fortement le processeur de shader, ainsi que la traduction à la volée des shaders en instructions machine.
===Un exemple : le jeu d’instruction du GPU de la Geforce 3===
La première carte graphique commerciale grand public à disposer d'une unité de vertex programmable est la Geforce 3. Celui-ci respectait le format de vertex shader 1.1. L'ensemble des informations à savoir sur cette unité est disponible dans l'article [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p149-lindholm.pdf "A user programmable vertex engine"], disponible sur le net. . Le processeur de cette carte était capable de gérer un seul type de données : les nombres flottants de norme IEEE754. Toutes les informations concernant la coordonnée d'une vertice, voire ses différentes couleurs, doivent être encodées en utilisant ces flottants.
Les processeurs de vertices de la Geforce 3 disposent de registres registres SIMD qui font 128 bits, soit 4 flottants de 32 bits. Elle contient 16 registres d'entrée, 16 registres de sortie, 32 registres généraux. La mémoire des constantes contient 512 "registres".
Le processeur de la Geforce 3 est capable d’exécuter 17 instructions différentes, dont voici les principales :
{|class="wikitable"
|-
!OpCode!!Nom!!Description
|-
! colspan="3" | Opérations mémoire
|-
|MOV||Move||vector -> vector
|-
|ARL||Address register load||miscellaneous
|-
! colspan="3" | Opérations arithmétiques
|-
|ADD||Add||vector -> vector
|-
|MUL||Multiply||vector -> vector
|-
|MAD||Multiply and add||vector -> vector
|-
|MIN||Minimum||vector -> vector
|-
|MAX||Maximum||vector -> vector
|-
|SLT||Set on less than||vector -> vector
|-
|SGE||Set on greater or equal||vector -> vector
|-
|LOG||Log base 2||miscellaneous
|-
|EXP||Exp base 2||miscellaneous
|-
|RCP||Reciprocal||scalar-> replicated scalar
|-
|RSQ||Reciprocal square root||scalar-> replicated scalar
|-
! colspan="3" | Opérations trigonométriques
|-
|DP3||3 term dot product||vector-> replicated scalar
|-
|DP4||4 term dot product||vector-> replicated scalar
|-
|DST||Distance||vector -> vector
|-
! colspan="3" | Opérations d'éclairage géométrique
|-
|LIT||Phong lighting||Calcule l'éclairage de Gouraud
|}
L'instruction la plus intéressante est clairement la dernière : elle éclaire un sommet, en utilisant un éclairage de Phong. Les autres instructions permettent d'implémenter un autre algorithme si besoin, mais cette forme d'éclairage est déjà là à la base.
Les autres instructions sont surtout des instructions arithmétiques : multiplications, additions, exponentielles, logarithmes, racines carrées, etc. Pour les instructions d'accès à la mémoire, on trouve une instruction MOV qui déplace le contenu d'un registre dans un autre et une instruction de calcul d'adresse, mais aucune instruction d'accès à la mémoire sur le processeur de la Geforce 3. Plus tard, les unités de ''vertex shader'' ont acquis la possibilité de lire des données dans une texture.
On remarque que la division est absente. Il faut dire que la contrainte qui veut que toutes ces instructions s’exécutent en un cycle d'horloge pose quelques problèmes avec la division, qui est une opération plutôt lourde en hardware. À la place, on trouve l'instruction RCP, capable de calculer 1/x, avec x un flottant. Cela permet ainsi de simuler une division : pour obtenir Y/X, il suffit de calculer 1/X avec RCP, et de multiplier le résultat par Y.
==La prédication et le SIMT==
Les cartes graphiques récentes peuvent effectuer des branchements, mais ceux-ci sont tout sauf performants. Dès qu'un branchement survient, le processeur est obligé de traiter chaque élément du vecteur un par un, au lieu de tous les traiter en même temps en parallèle. Les performances s'en ressentent, ce qui fait que les branchements sont à éviter le plus possible. Pour améliorer la gestion des conditions, les cartes graphiques modernes incorporent des instructions spécialisées qui permettent de remplacer des codes remplis de branchements par des codes plus simples, compatibles avec l'organisation des données en vecteurs.
Si on met de côté le support de certaines instructions courantes, comme la valeur absolue, ou le calcul du minimum/maximum, la technique la plus importante est la technique dite de '''prédication'''. L'idée est que quand une instruction effectue un calcul sur un ou deux vecteurs, certains éléments du vecteur sont ignorés. Les éléments à ignorer sont choisis suivant le résultat d'une instruction de comparaison, qui effectue un test : les éléments pour lesquels ce test est respecté sont pris en compte, ceux qui ne passent pas le test sont ignorés.
Pour donner un exemple d'utilisation, imaginons que l'on ait un vecteur dans lequel on veut remplacer toutes les valeurs négatives par des 0. Dans ce cas, on utilise :
* une instruction de comparaison, qui compare chaque élément du vecteur avec 0 et génère plusieurs bits de résultat ;
* suivi d'une instruction à prédicat qui met à zéro les éléments pour lesquels les bits de résultat précédents sont à 1.
Elle est implémentée grâce à un registre appelé le '''''Vector Mask Register'''''. Celui-ci permet de stocker des informations qui permettront de sélectionner certaines données et pas d'autres pour faire notre calcul. Il est mis à jour par des instructions de comparaison. le ''Vector Mask Register'' stocke un bit pour chaque flottant présent dans le vecteur à traiter, bit qui indique s'il faut appliquer l'instruction sur ce flottant. Si ce bit est à 1, notre instruction doit s’exécuter sur la donnée associée à ce bit. Sinon, notre instruction ne doit pas la modifier. On peut ainsi traiter seulement une partie des registres stockant des vecteurs SIMD.
[[File:Vector mask register.png|centre|vignette|upright=2.0|''Vector mask register'']]
===La prédication avec une pile SIMT===
Au niveau du jeu d’instruction, les architectures SIMT implémentent de la prédication, sous une forme améliorée. Les processeurs SIMT actuels sont surtout utilisées sur les processeurs intégrés aux cartes graphiques. Et ces derniers gèrent très mal les branchements, et encore : beaucoup de cartes graphiques, même récentes, ne gèrent tout simplement pas les branchements. Elles doivent donc se débrouiller avec uniquement la prédication, là où les processeurs SIMD utilisent des branchements normaux en complément de la prédication. Insistons sur le fait que cet usage exclusif de la prédication n'est présent que sur une sous-partie des architectures SIMT, le seul exemple que l'auteur de ce wikilivre connait étant celui des cartes graphiques.
Les architectures SIMT sans branchements doivent donc trouver des solutions pour gérer les structures de contrôle imbriquées, à savoir une boucle placée à l'intérieur d'une autre boucle, un IF...ELSE dans un autre IF...ELSE, etc. Elles utilisent pour cela la prédication, combinée avec des mécanismes annexes. Le premier d'entre eux est l'usage de plusieurs registres de masques organisés d'une manière bien précise, l'autre est l'usage de compteurs d'activité. Voyons ces deux techniques.
La '''pile de masques''' remplace le ou les registres de masque. Sans elle, le processeur SIMD incorpore un registre de masque qui est adressé implicitement ou explicitement. Éventuellement, le processeur peut contenir plusieurs registres de masque séparés adressables via un nom de registre. Avec elle, le processeur SIMD incorpore plusieurs registres de masque organisé en pile. Le registre de masque est donc remplacé par une mémoire LIFO, une pile, dans laquelle plusieurs masques sont empilés.
Le tout forme une pile, similaire à la pile d'appel, sauf qu'elle est utilisée pour empiler des masques. Un masque est calculé et empilé à chaque entrée dans une structure de contrôle, puis dépilé une fois la structure de contrôle exécutée. L'empilement et le dépilement des masques est effectué par des instructions PUSH et POP, présentes dans le jeu d'instruction du processeur SIMD.
Le calcul des masques doit répondre à plusieurs impératifs.
* Premièrement, chaque masque se calcule en faisant un ET entre le masque précédent et le masque calculé par l'instruction de test. Cela permet de ne pas réveiller d’élément au beau milieu d'une structure imbriquée. Si in IF désactive certains éléments du vecteur, une condition imbriquée dans ce IF ne doit pas réveiller cet élément. Le fait de faire un ET entre les masques garantit cela.
* Deuxièmement, les masques doivent être empilés et dépilés correctement. Au moment de rentrer dans une structure de contrôle, on effectue une instruction de test associée à la structure de contrôle, qui calcule un masque, et on empile le masque calculé. Au moment de sortir de la structure de contrôle, on dépile le masque en question.
L'implémentation demande d'utiliser une mémoire LIFO pour stocker la pile de masques, et quelques circuits annexes. Il faut notamment un circuit relié à l'ALU qui récupère les conditions, les résultats des comparaisons, et qui effectue le ET pour combiner les masques.
Pour donner un exemple, prenons le code suivant, qui est volontairement simpliste et ne sert qu'à des fins d'explication :
<syntaxhighlight lang="c">
if ( condition 1 )
{
if ( condition 2 )
{
...
}
else
{
...
}
Autres instructions
}
Instructions après le IF...
</syntaxhighlight>
Imaginons que l'on traite des vecteurs de 8 éléments.
Pour le vecteur considéré, la première condition (a > 0) n'est respectée que par les 4 premiers éléments. L'instruction de condition calcule alors le masque correspondant : 1111 0000. Le masque est alors calculé, puis empilé au sommet de la pile.
La seconde instruction de test, qui teste la variable b, est maintenant valide pour les 4 bits du milieu du masque. Mais n'allez pas croire que le masque correspondant soit 0011 11100 : il faut tenir compte de la condition précédente, qui a éliminé les 4 derniers éléments. Pour cela, on fait un ET logique entre le masque précédent, et le masque calculé par la condition. Le masque au sommet de la pile est donc lu, combiné avec le masque calculé par l'instruction, ce qui donne le masque final. Le masque final est alors empilé au sommet de la pile.
On exécute alors l'instruction du IF, en tenant compte du masque qui est au sommet de la pile. Si le IF était plus compliqué, toutes les instructions suivantes tiendraient compte du masque. En fait, le masque est pris en compte tant qu'il n'est pas dépilé. Une fois que le IF est terminé, le masque est dépilé.
On passe alors au ELSE, et rebelotte. Le masque pour le ELSE est calculé en combinant le masque au sommet de la pile avec la condition du ELSE. Le masque au sommet de la pile est celui calculé à l'entrée du premier IF, pas le second qui a été dépilé. Les instructions du ELSE sont alors exécutées en tenant compte de ce masque. Une fois qu'elles sont toutes exécutées, le masque est dépilé.
Puis vient l'exécution des instructions après le ELSE. Elles utilisent le masque empilé au sommet de la pile, qui correspond à celui à l'entrée du IF.
Puis vient le moment d'exécuter les instructions après le IF : pas de masque, on exécute sur tout le vecteur.
===Les compteurs d'activité===
Une variante de la technique précédente remplace la pile de masques par des '''compteurs d'activité'''. La technique est similaire, si ce n'est qu'elle utilise moins de circuits. Avant , on avait une pile de masques de même taille, dont les bits sont à 0 ou 1 suivant que la condition est remplie. La pile de masque ressemble donc à ceci :
{|class="wikitable"
|-
! masque 1
| 1 || 1 || 1 || 1
|-
! masque 2
| 0 || 1 || 1 || 1
|-
! masque 3
| 0 || 1 || 1 || 1
|-
! masque 4
| 0 || 0 || 0 || 1
|-
! masque 1
| colspan="4" | vide
|}
Une manière équivalente de représenter cette pile de masque est de compter combien de bits sont à 0 dans chaque colonne. Attention : j'ai bien dit à 0 ! On obtient alors :
{|class="wikitable"
|-
! masque 1
| 3 || 1 || 1 || 0
|}
Et c'est le principe caché derrière la technique des compteurs d'activité. Chaque élément dans un vecteur, chaque place, se voit attribuer un compteur. Un compteur non-nul indique qu'il ne faut pas prendre en compte l’élément. Ce n'est qu'une fois que le compteur est nul que l'on effectue des opérations sur l’élément associé du vecteur.
À chaque fois qu'on entre dans une structure de contrôle, on teste une condition sur chaque élément. Si la condition est respectée pour un élément, alors le compteur ne change pas. Mais si la condition n'est pas respectée, alors on incrémente le compteur associé. En sortant de la structure de contrôle, on décrémente le compteur associé. Notons que les compteurs qui n'ont pas été incrémentés en entrant dans la structure de contrôle ne sont pas décrémentés en sortant. En clair, là où on empilait/dépilait un masque, on se contente d'incrémenter/décrémenter un compteur.
Utiliser un compteur en lieu et place d'une colonne entière dans la pile de masque utilise moins de bits. Et c'est sans doute pour cette raison que certaines cartes graphiques, comme les cartes graphiques intégrées d'Intel depuis 2004, utilisent cette technique.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes accélératrices 3D
| prevText=Les cartes accélératrices 3D
| next=La microarchitecture des processeurs de shaders
| nextText=La microarchitecture des processeurs de shaders
}}
{{autocat}}
kx1qw12u0qwskfonn92bdodu9uxd2k5
Les cartes graphiques/Le rendu d'une scène 3D : concepts de base
0
79234
763382
763379
2026-04-10T13:46:25Z
Mewtow
31375
/* La lumière incidente : le terme géométrique */
763382
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D.
==Les bases du rendu 3D==
Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme.
===Les objets 3D et leur géométrie===
[[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]]
Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Un triangle est donc composé de 9 coordonnées, 3 par sommet.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets, pouvant contenir une certaine redondance ou des informations en plus. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie.
===La caméra : le point de vue depuis l'écran===
Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]]
La majorité des jeux vidéos ajoutent deux plans :
* un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches.
* Un ''far plane'', un'''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains.
Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels.
===Les textures===
Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]]
Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres.
Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique.
Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal.
La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''.
La rastérisation est structurée autour de trois étapes principales :
* une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D ;
* une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles ;
* une étape de '''rastérisation''' qui effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit.
==Le calcul de la géométrie==
Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective.
===Les trois étapes de transformation===
La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour.
De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]]
Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z).
[[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]]
Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces.
===Les changements de coordonnées se font via des multiplications de matrices===
Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc.
Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail.
Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble.
Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc.
Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps.
==L'élimination des surfaces cachées==
Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''.
Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre.
===Les différentes formes de ''culling''/''clipping''===
La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable.
[[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]]
Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]]
Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger.
L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z.
===L'algorithme du peintre===
Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal.
Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU.
Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment.
===Le tampon de profondeur===
Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]]
Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives.
Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective.
Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures.
===Le rendu en fil de fer===
[[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]]
Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]]
|}
Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet.
L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer.
Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres.
===Le rendu à primitives colorées===
[[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]]
Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]]
La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique.
Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés.
Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche.
Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse.
L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes.
Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''.
{|class="wikitable"
|-
! Géométrie
| Processeurs dédiés programmé pour émuler le pipeline graphique
|-
! Tri des quads du plus lointain au plus proche
| Processeur principal (implémentation logicielle)
|-
! Application des textures
| ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''.
|}
L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque.
Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn.
===Le placage de textures inverse===
Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet.
[[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]]
Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés.
[[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]]
Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé.
Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités.
L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes.
==L'éclairage d'une scène 3D==
L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante.
===La lumière incidente : le terme géométrique===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante.
La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
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:Angle of incidence.svg|centre|vignette|upright=2|Angle of incidence]]
Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière 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. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que l'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''.
Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface n'est pas parfaitement lisse. Elle a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Reste à calculer la réflexion diffuse. Il y a plusieurs algorithmes pour cela, mais la solution la plus simple est de donner une '''couleur diffuse''' à chaque sommet. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants :
* L est le vecteur pour la lumière incidente ;
* N est la normale du sommet ;
* I est l'intensité de la source de lumière ;
* <math>K_d</math> est la couleur diffuse.
: <math>\text{Illumination diffuse} = K_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math>
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Maintenant, il faut savoir que de nombreux matériaux ne se limitent pas à de la réflexion diffuse. Ils ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, mais réfléchit la lumière dans des directions très proches. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
La réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Évidemment, cela demande de calculer un vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. La réflexion spéculaire est une fonction qui dépend de l'angle entre ce vecteur R, et la direction du regard notée V. Et il faut aussi tenir compte du terme géométrique.
: <math>\text{Illumination spéculaire} = \left[ I \times (\vec{N} \cdot \vec{L}) \right] \times f(\vec{R} \cdot \vec{V}) </math>
La fonction en question peut être très compliquée, mais le cas classique est celui des ''Phong material''. Un '''''Phong material''''' cumule à la fois une réflexion diffuse et une réflexion spéculaire, la réflexion spéculaire étant calculée avec une formule mathématique bien précise. Les deux sont présentes en des proportions différentes, qu'on peut configurer pour chaque ''material''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses.
Le calcul exact de la lumière spéculaire avec ce genre de ''material'' est un peu complexe. Il a besoin d'une '''couleur spéculaire''', qui est équivalente à la couleur diffuse, mais pour la réflexion spéculaire. Ensuite, il y a besoin de connaitre l'angle entre R et V. Simplement multiplier les deux donnerait un mauvais résultat, car la lumière spéculaire ne serait pas atténuée assez vite, quand on s'éloigne de la direction de réflexion maximale. Aussi, on élève ce cosinus à une certaine puissance. L'exposant dépend du ''matérial'' considéré, c'est un paramètre qu'on peut faire varier à loisir. Le résultat final est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})^{ \alpha}</math>
: Le terme géométrique est en réalité pris en compte implicitement, je ne rentre pas dan le détail.
A cela, il faut rajouter la lumière ambiante, qui est simplement obtenue en multipliant l'intensité de la lumière ambiante par la couleur du sommet. La couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la '''couleur ambiante'''.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Avec tout ce qui a été dit précédemment, l'éclairage était calculé pour chaque sommet. Il attribuait une illumination/couleur à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation.
L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
La distinction de l'éclairage par pixel et par sommet est souvent complété avec une troisième possibilité : l''''éclairage par triangle'''. L'idée est de donner une couleur/illumination par triangle, et non par sommet. Les trois possibilités sont souvent appelées l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. A la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
La différence entre les trois algorithmes se voit assez facilement à lécran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calcul d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
7cfk958a2scsrlpd9n56448iuk05vmu
763383
763382
2026-04-10T13:47:00Z
Mewtow
31375
/* La lumière incidente : le terme géométrique */
763383
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D.
==Les bases du rendu 3D==
Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme.
===Les objets 3D et leur géométrie===
[[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]]
Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Un triangle est donc composé de 9 coordonnées, 3 par sommet.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets, pouvant contenir une certaine redondance ou des informations en plus. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie.
===La caméra : le point de vue depuis l'écran===
Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]]
La majorité des jeux vidéos ajoutent deux plans :
* un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches.
* Un ''far plane'', un'''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains.
Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels.
===Les textures===
Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]]
Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres.
Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique.
Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal.
La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''.
La rastérisation est structurée autour de trois étapes principales :
* une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D ;
* une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles ;
* une étape de '''rastérisation''' qui effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit.
==Le calcul de la géométrie==
Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective.
===Les trois étapes de transformation===
La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour.
De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]]
Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z).
[[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]]
Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces.
===Les changements de coordonnées se font via des multiplications de matrices===
Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc.
Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail.
Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble.
Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc.
Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps.
==L'élimination des surfaces cachées==
Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''.
Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre.
===Les différentes formes de ''culling''/''clipping''===
La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable.
[[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]]
Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]]
Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger.
L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z.
===L'algorithme du peintre===
Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal.
Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU.
Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment.
===Le tampon de profondeur===
Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]]
Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives.
Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective.
Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures.
===Le rendu en fil de fer===
[[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]]
Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]]
|}
Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet.
L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer.
Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres.
===Le rendu à primitives colorées===
[[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]]
Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]]
La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique.
Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés.
Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche.
Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse.
L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes.
Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''.
{|class="wikitable"
|-
! Géométrie
| Processeurs dédiés programmé pour émuler le pipeline graphique
|-
! Tri des quads du plus lointain au plus proche
| Processeur principal (implémentation logicielle)
|-
! Application des textures
| ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''.
|}
L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque.
Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn.
===Le placage de textures inverse===
Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet.
[[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]]
Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés.
[[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]]
Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé.
Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités.
L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes.
==L'éclairage d'une scène 3D==
L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante.
===La lumière incidente : le terme géométrique===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante.
La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
[[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. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que l'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''.
Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface n'est pas parfaitement lisse. Elle a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Reste à calculer la réflexion diffuse. Il y a plusieurs algorithmes pour cela, mais la solution la plus simple est de donner une '''couleur diffuse''' à chaque sommet. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants :
* L est le vecteur pour la lumière incidente ;
* N est la normale du sommet ;
* I est l'intensité de la source de lumière ;
* <math>K_d</math> est la couleur diffuse.
: <math>\text{Illumination diffuse} = K_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math>
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Maintenant, il faut savoir que de nombreux matériaux ne se limitent pas à de la réflexion diffuse. Ils ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, mais réfléchit la lumière dans des directions très proches. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
La réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Évidemment, cela demande de calculer un vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. La réflexion spéculaire est une fonction qui dépend de l'angle entre ce vecteur R, et la direction du regard notée V. Et il faut aussi tenir compte du terme géométrique.
: <math>\text{Illumination spéculaire} = \left[ I \times (\vec{N} \cdot \vec{L}) \right] \times f(\vec{R} \cdot \vec{V}) </math>
La fonction en question peut être très compliquée, mais le cas classique est celui des ''Phong material''. Un '''''Phong material''''' cumule à la fois une réflexion diffuse et une réflexion spéculaire, la réflexion spéculaire étant calculée avec une formule mathématique bien précise. Les deux sont présentes en des proportions différentes, qu'on peut configurer pour chaque ''material''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses.
Le calcul exact de la lumière spéculaire avec ce genre de ''material'' est un peu complexe. Il a besoin d'une '''couleur spéculaire''', qui est équivalente à la couleur diffuse, mais pour la réflexion spéculaire. Ensuite, il y a besoin de connaitre l'angle entre R et V. Simplement multiplier les deux donnerait un mauvais résultat, car la lumière spéculaire ne serait pas atténuée assez vite, quand on s'éloigne de la direction de réflexion maximale. Aussi, on élève ce cosinus à une certaine puissance. L'exposant dépend du ''matérial'' considéré, c'est un paramètre qu'on peut faire varier à loisir. Le résultat final est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})^{ \alpha}</math>
: Le terme géométrique est en réalité pris en compte implicitement, je ne rentre pas dan le détail.
A cela, il faut rajouter la lumière ambiante, qui est simplement obtenue en multipliant l'intensité de la lumière ambiante par la couleur du sommet. La couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la '''couleur ambiante'''.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Avec tout ce qui a été dit précédemment, l'éclairage était calculé pour chaque sommet. Il attribuait une illumination/couleur à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation.
L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
La distinction de l'éclairage par pixel et par sommet est souvent complété avec une troisième possibilité : l''''éclairage par triangle'''. L'idée est de donner une couleur/illumination par triangle, et non par sommet. Les trois possibilités sont souvent appelées l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. A la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
La différence entre les trois algorithmes se voit assez facilement à lécran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calcul d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
k8t2ir55hz7t4l0idod8bdlrzrd62m9
763384
763383
2026-04-10T13:52:17Z
Mewtow
31375
/* La réflexion de la lumière sur la surface */
763384
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D.
==Les bases du rendu 3D==
Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme.
===Les objets 3D et leur géométrie===
[[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]]
Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Un triangle est donc composé de 9 coordonnées, 3 par sommet.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets, pouvant contenir une certaine redondance ou des informations en plus. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie.
===La caméra : le point de vue depuis l'écran===
Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]]
La majorité des jeux vidéos ajoutent deux plans :
* un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches.
* Un ''far plane'', un'''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains.
Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels.
===Les textures===
Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]]
Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres.
Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique.
Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal.
La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''.
La rastérisation est structurée autour de trois étapes principales :
* une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D ;
* une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles ;
* une étape de '''rastérisation''' qui effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit.
==Le calcul de la géométrie==
Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective.
===Les trois étapes de transformation===
La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour.
De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]]
Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z).
[[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]]
Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces.
===Les changements de coordonnées se font via des multiplications de matrices===
Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc.
Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail.
Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble.
Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc.
Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps.
==L'élimination des surfaces cachées==
Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''.
Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre.
===Les différentes formes de ''culling''/''clipping''===
La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable.
[[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]]
Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]]
Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger.
L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z.
===L'algorithme du peintre===
Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal.
Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU.
Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment.
===Le tampon de profondeur===
Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]]
Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives.
Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective.
Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures.
===Le rendu en fil de fer===
[[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]]
Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]]
|}
Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet.
L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer.
Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres.
===Le rendu à primitives colorées===
[[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]]
Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]]
La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique.
Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés.
Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche.
Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse.
L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes.
Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''.
{|class="wikitable"
|-
! Géométrie
| Processeurs dédiés programmé pour émuler le pipeline graphique
|-
! Tri des quads du plus lointain au plus proche
| Processeur principal (implémentation logicielle)
|-
! Application des textures
| ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''.
|}
L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque.
Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn.
===Le placage de textures inverse===
Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet.
[[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]]
Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés.
[[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]]
Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé.
Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités.
L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes.
==L'éclairage d'une scène 3D==
L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante.
===La lumière incidente : le terme géométrique===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante.
La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
[[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. Les lois de l'optique géométrique nous disent que l'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''.
Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. 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èren 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>
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Maintenant, il faut savoir que de nombreux matériaux ne se limitent pas à de la réflexion diffuse. Ils ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, mais réfléchit la lumière dans des directions très proches. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
La réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Évidemment, cela demande de calculer un vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. La réflexion spéculaire est une fonction qui dépend de l'angle entre ce vecteur R, et la direction du regard notée V. Et il faut aussi tenir compte du terme géométrique.
: <math>\text{BRDFspéculaire} = f(\vec{R} \cdot \vec{V}) </math>
La fonction en question peut être très compliquée, mais le cas classique est celui des ''Phong material''. Un '''''Phong material''''' cumule à la fois une réflexion diffuse et une réflexion spéculaire, la réflexion spéculaire étant calculée avec une formule mathématique bien précise. Les deux sont présentes en des proportions différentes, qu'on peut configurer pour chaque ''material''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses.
Le calcul exact de la lumière spéculaire avec ce genre de ''material'' est un peu complexe. Il a besoin d'une '''couleur spéculaire''', qui est équivalente à la couleur diffuse, mais pour la réflexion spéculaire. Ensuite, il y a besoin de connaitre l'angle entre R et V. Simplement multiplier les deux donnerait un mauvais résultat, car la lumière spéculaire ne serait pas atténuée assez vite, quand on s'éloigne de la direction de réflexion maximale. Aussi, on élève ce cosinus à une certaine puissance. L'exposant dépend du ''matérial'' considéré, c'est un paramètre qu'on peut faire varier à loisir. Le résultat final est le suivant :
: <math>\text{BRDFspéculaire} = (\vec{R} \cdot \vec{v})^{ \alpha}</math>
: Le terme géométrique est en réalité pris en compte implicitement, je ne rentre pas dan le détail.
A cela, il faut rajouter la lumière ambiante, qui est simplement obtenue en multipliant l'intensité de la lumière ambiante par la couleur du sommet. La couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la '''couleur ambiante'''.
: <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = C_a \times I_a + I \times \left[ C_d \times (\vec{N} \cdot \vec{L}) + C_s \times (\vec{R} \cdot \vec{v}) \right]</math>
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Avec tout ce qui a été dit précédemment, l'éclairage était calculé pour chaque sommet. Il attribuait une illumination/couleur à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation.
L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
La distinction de l'éclairage par pixel et par sommet est souvent complété avec une troisième possibilité : l''''éclairage par triangle'''. L'idée est de donner une couleur/illumination par triangle, et non par sommet. Les trois possibilités sont souvent appelées l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. A la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
La différence entre les trois algorithmes se voit assez facilement à lécran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calcul d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
9zutrwvfq0m3nfd8a7xop8gw6su9m5s
763385
763384
2026-04-10T14:10:49Z
Mewtow
31375
/* La réflexion de la lumière sur la surface */
763385
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D.
==Les bases du rendu 3D==
Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme.
===Les objets 3D et leur géométrie===
[[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]]
Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Un triangle est donc composé de 9 coordonnées, 3 par sommet.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets, pouvant contenir une certaine redondance ou des informations en plus. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie.
===La caméra : le point de vue depuis l'écran===
Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]]
La majorité des jeux vidéos ajoutent deux plans :
* un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches.
* Un ''far plane'', un'''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains.
Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels.
===Les textures===
Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]]
Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres.
Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique.
Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal.
La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''.
La rastérisation est structurée autour de trois étapes principales :
* une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D ;
* une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles ;
* une étape de '''rastérisation''' qui effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit.
==Le calcul de la géométrie==
Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective.
===Les trois étapes de transformation===
La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour.
De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]]
Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z).
[[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]]
Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces.
===Les changements de coordonnées se font via des multiplications de matrices===
Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc.
Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail.
Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble.
Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc.
Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps.
==L'élimination des surfaces cachées==
Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''.
Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre.
===Les différentes formes de ''culling''/''clipping''===
La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable.
[[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]]
Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]]
Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger.
L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z.
===L'algorithme du peintre===
Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal.
Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU.
Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment.
===Le tampon de profondeur===
Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]]
Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives.
Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective.
Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures.
===Le rendu en fil de fer===
[[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]]
Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]]
|}
Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet.
L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer.
Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres.
===Le rendu à primitives colorées===
[[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]]
Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]]
La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique.
Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés.
Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche.
Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse.
L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes.
Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''.
{|class="wikitable"
|-
! Géométrie
| Processeurs dédiés programmé pour émuler le pipeline graphique
|-
! Tri des quads du plus lointain au plus proche
| Processeur principal (implémentation logicielle)
|-
! Application des textures
| ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''.
|}
L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque.
Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn.
===Le placage de textures inverse===
Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet.
[[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]]
Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés.
[[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]]
Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé.
Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités.
L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes.
==L'éclairage d'une scène 3D==
L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante.
===La lumière incidente : le terme géométrique===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante.
La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
[[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>
[[File:Haze-Reflection-2.png|vignette|upright=1|Réflexion spéculaire.]]
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:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
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.
: <math>\text{BRDF spéculaire} = f(\vec{R} \cdot \vec{V}) </math>
La fonction en question peut être très compliquée, mais le cas classique est celui de la réflexion spéculaire de Phong. Un ''material'' de Phong a une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. La reflexion est calculée à partir du cosinus de l'angle entre R et V. Simplement multiplier les deux donnerait un mauvais résultat, car la lumière spéculaire ne serait pas atténuée assez vite, quand on s'éloigne de la direction de réflexion maximale. Aussi, on élève ce cosinus à une certaine puissance, dont l'exposant dépend du ''material'' considéré.
: <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
Un '''''Phong material''''' cumule à la fois une réflexion ambiante, une réflexion diffuse et une réflexion spéculaire. En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = C_a \times I_a + I \times \left[ C_d \times (\vec{N} \cdot \vec{L}) + C_s \times (\vec{R} \cdot \vec{v}) \right]</math>
: Le terme géométrique est en réalité pris en compte implicitement pour le terme spéculaire, je ne rentre pas dan le détail.
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Avec tout ce qui a été dit précédemment, l'éclairage était calculé pour chaque sommet. Il attribuait une illumination/couleur à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation.
L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
La distinction de l'éclairage par pixel et par sommet est souvent complété avec une troisième possibilité : l''''éclairage par triangle'''. L'idée est de donner une couleur/illumination par triangle, et non par sommet. Les trois possibilités sont souvent appelées l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. A la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
La différence entre les trois algorithmes se voit assez facilement à lécran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calcul d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
nq4ive1wo2zhdz1fj1txqjupgqa5266
763386
763385
2026-04-10T14:13:06Z
Mewtow
31375
/* Les sources de lumière et les couleurs associées */
763386
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D.
==Les bases du rendu 3D==
Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme.
===Les objets 3D et leur géométrie===
[[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]]
Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Un triangle est donc composé de 9 coordonnées, 3 par sommet.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets, pouvant contenir une certaine redondance ou des informations en plus. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie.
===La caméra : le point de vue depuis l'écran===
Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]]
La majorité des jeux vidéos ajoutent deux plans :
* un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches.
* Un ''far plane'', un'''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains.
Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels.
===Les textures===
Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]]
Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres.
Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique.
Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal.
La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''.
La rastérisation est structurée autour de trois étapes principales :
* une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D ;
* une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles ;
* une étape de '''rastérisation''' qui effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit.
==Le calcul de la géométrie==
Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective.
===Les trois étapes de transformation===
La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour.
De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]]
Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z).
[[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]]
Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces.
===Les changements de coordonnées se font via des multiplications de matrices===
Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc.
Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail.
Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble.
Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc.
Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps.
==L'élimination des surfaces cachées==
Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''.
Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre.
===Les différentes formes de ''culling''/''clipping''===
La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable.
[[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]]
Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]]
Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger.
L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z.
===L'algorithme du peintre===
Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal.
Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU.
Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment.
===Le tampon de profondeur===
Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]]
Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives.
Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective.
Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures.
===Le rendu en fil de fer===
[[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]]
Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]]
|}
Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet.
L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer.
Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres.
===Le rendu à primitives colorées===
[[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]]
Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]]
La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique.
Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés.
Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche.
Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse.
L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes.
Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''.
{|class="wikitable"
|-
! Géométrie
| Processeurs dédiés programmé pour émuler le pipeline graphique
|-
! Tri des quads du plus lointain au plus proche
| Processeur principal (implémentation logicielle)
|-
! Application des textures
| ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''.
|}
L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque.
Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn.
===Le placage de textures inverse===
Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet.
[[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]]
Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés.
[[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]]
Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé.
Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités.
L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes.
==L'éclairage d'une scène 3D==
L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante.
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 par l'intensité de la lumière ambiante. La couleur utilisée est appelée la '''couleur ambiante''', elle est différente de la couleur utilisée pour calculer l'éclairage avec les autres sources de lumière.
: <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
===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>
[[File:Haze-Reflection-2.png|vignette|upright=1|Réflexion spéculaire.]]
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:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
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.
: <math>\text{BRDF spéculaire} = f(\vec{R} \cdot \vec{V}) </math>
La fonction en question peut être très compliquée, mais le cas classique est celui de la réflexion spéculaire de Phong. Un ''material'' de Phong a une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. La reflexion est calculée à partir du cosinus de l'angle entre R et V. Simplement multiplier les deux donnerait un mauvais résultat, car la lumière spéculaire ne serait pas atténuée assez vite, quand on s'éloigne de la direction de réflexion maximale. Aussi, on élève ce cosinus à une certaine puissance, dont l'exposant dépend du ''material'' considéré.
: <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
Un '''''Phong material''''' cumule à la fois une réflexion ambiante, une réflexion diffuse et une réflexion spéculaire. En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = C_a \times I_a + I \times \left[ C_d \times (\vec{N} \cdot \vec{L}) + C_s \times (\vec{R} \cdot \vec{v}) \right]</math>
: Le terme géométrique est en réalité pris en compte implicitement pour le terme spéculaire, je ne rentre pas dan le détail.
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Avec tout ce qui a été dit précédemment, l'éclairage était calculé pour chaque sommet. Il attribuait une illumination/couleur à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation.
L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
La distinction de l'éclairage par pixel et par sommet est souvent complété avec une troisième possibilité : l''''éclairage par triangle'''. L'idée est de donner une couleur/illumination par triangle, et non par sommet. Les trois possibilités sont souvent appelées l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. A la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
La différence entre les trois algorithmes se voit assez facilement à lécran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calcul d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
qxn2ax5bnvyonm17d2hhurryb0vy8r7
763387
763386
2026-04-10T14:13:57Z
Mewtow
31375
/* Les sources de lumière et les couleurs associées */
763387
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D.
==Les bases du rendu 3D==
Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme.
===Les objets 3D et leur géométrie===
[[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]]
Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Un triangle est donc composé de 9 coordonnées, 3 par sommet.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets, pouvant contenir une certaine redondance ou des informations en plus. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie.
===La caméra : le point de vue depuis l'écran===
Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]]
La majorité des jeux vidéos ajoutent deux plans :
* un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches.
* Un ''far plane'', un'''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains.
Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels.
===Les textures===
Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]]
Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres.
Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique.
Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal.
La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''.
La rastérisation est structurée autour de trois étapes principales :
* une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D ;
* une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles ;
* une étape de '''rastérisation''' qui effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit.
==Le calcul de la géométrie==
Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective.
===Les trois étapes de transformation===
La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour.
De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]]
Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z).
[[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]]
Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces.
===Les changements de coordonnées se font via des multiplications de matrices===
Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc.
Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail.
Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble.
Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc.
Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps.
==L'élimination des surfaces cachées==
Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''.
Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre.
===Les différentes formes de ''culling''/''clipping''===
La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable.
[[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]]
Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]]
Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger.
L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z.
===L'algorithme du peintre===
Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal.
Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU.
Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment.
===Le tampon de profondeur===
Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]]
Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives.
Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective.
Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures.
===Le rendu en fil de fer===
[[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]]
Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]]
|}
Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet.
L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer.
Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres.
===Le rendu à primitives colorées===
[[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]]
Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]]
La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique.
Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés.
Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche.
Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse.
L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes.
Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''.
{|class="wikitable"
|-
! Géométrie
| Processeurs dédiés programmé pour émuler le pipeline graphique
|-
! Tri des quads du plus lointain au plus proche
| Processeur principal (implémentation logicielle)
|-
! Application des textures
| ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''.
|}
L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque.
Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn.
===Le placage de textures inverse===
Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet.
[[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]]
Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés.
[[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]]
Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé.
Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités.
L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes.
==L'éclairage d'une scène 3D==
L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante.
===La lumière incidente : le terme géométrique===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante.
La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
[[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>
[[File:Haze-Reflection-2.png|vignette|upright=1|Réflexion spéculaire.]]
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:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
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.
: <math>\text{BRDF spéculaire} = f(\vec{R} \cdot \vec{V}) </math>
La fonction en question peut être très compliquée, mais le cas classique est celui de la réflexion spéculaire de Phong. Un ''material'' de Phong a une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. La reflexion est calculée à partir du cosinus de l'angle entre R et V. Simplement multiplier les deux donnerait un mauvais résultat, car la lumière spéculaire ne serait pas atténuée assez vite, quand on s'éloigne de la direction de réflexion maximale. Aussi, on élève ce cosinus à une certaine puissance, dont l'exposant dépend du ''material'' considéré.
: <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
Un '''''Phong material''''' cumule à la fois une réflexion ambiante, une réflexion diffuse et une réflexion spéculaire. En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = C_a \times I_a + I \times \left[ C_d \times (\vec{N} \cdot \vec{L}) + C_s \times (\vec{R} \cdot \vec{v}) \right]</math>
: Le terme géométrique est en réalité pris en compte implicitement pour le terme spéculaire, je ne rentre pas dan le détail.
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Avec tout ce qui a été dit précédemment, l'éclairage était calculé pour chaque sommet. Il attribuait une illumination/couleur à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation.
L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
La distinction de l'éclairage par pixel et par sommet est souvent complété avec une troisième possibilité : l''''éclairage par triangle'''. L'idée est de donner une couleur/illumination par triangle, et non par sommet. Les trois possibilités sont souvent appelées l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. A la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
La différence entre les trois algorithmes se voit assez facilement à lécran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calcul d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
nq4ive1wo2zhdz1fj1txqjupgqa5266
763388
763387
2026-04-10T14:16:21Z
Mewtow
31375
/* La réflexion de la lumière sur la surface */
763388
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D.
==Les bases du rendu 3D==
Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme.
===Les objets 3D et leur géométrie===
[[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]]
Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Un triangle est donc composé de 9 coordonnées, 3 par sommet.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets, pouvant contenir une certaine redondance ou des informations en plus. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie.
===La caméra : le point de vue depuis l'écran===
Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]]
La majorité des jeux vidéos ajoutent deux plans :
* un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches.
* Un ''far plane'', un'''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains.
Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels.
===Les textures===
Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]]
Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres.
Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique.
Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal.
La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''.
La rastérisation est structurée autour de trois étapes principales :
* une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D ;
* une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles ;
* une étape de '''rastérisation''' qui effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit.
==Le calcul de la géométrie==
Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective.
===Les trois étapes de transformation===
La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour.
De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]]
Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z).
[[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]]
Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces.
===Les changements de coordonnées se font via des multiplications de matrices===
Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc.
Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail.
Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble.
Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc.
Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps.
==L'élimination des surfaces cachées==
Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''.
Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre.
===Les différentes formes de ''culling''/''clipping''===
La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable.
[[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]]
Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]]
Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger.
L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z.
===L'algorithme du peintre===
Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal.
Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU.
Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment.
===Le tampon de profondeur===
Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]]
Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives.
Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective.
Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures.
===Le rendu en fil de fer===
[[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]]
Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]]
|}
Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet.
L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer.
Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres.
===Le rendu à primitives colorées===
[[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]]
Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]]
La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique.
Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés.
Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche.
Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse.
L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes.
Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''.
{|class="wikitable"
|-
! Géométrie
| Processeurs dédiés programmé pour émuler le pipeline graphique
|-
! Tri des quads du plus lointain au plus proche
| Processeur principal (implémentation logicielle)
|-
! Application des textures
| ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''.
|}
L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque.
Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn.
===Le placage de textures inverse===
Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet.
[[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]]
Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés.
[[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]]
Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé.
Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités.
L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes.
==L'éclairage d'une scène 3D==
L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante.
===La lumière incidente : le terme géométrique===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante.
La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
[[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.
[[File:Haze-Reflection-2.png|vignette|upright=1|Réflexion spéculaire.]]
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:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
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.
: <math>\text{BRDF spéculaire} = f(\vec{R} \cdot \vec{V}) </math>
La fonction en question peut être très compliquée, mais le cas classique est celui de la réflexion spéculaire de Phong. Un ''material'' de Phong a une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. La reflexion est calculée à partir du cosinus de l'angle entre R et V. Simplement multiplier les deux donnerait un mauvais résultat, car la lumière spéculaire ne serait pas atténuée assez vite, quand on s'éloigne de la direction de réflexion maximale. Aussi, on élève ce cosinus à une certaine puissance, dont l'exposant dépend du ''material'' considéré.
: <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
Un '''''Phong material''''' cumule à la fois une réflexion ambiante, une réflexion diffuse et une réflexion spéculaire. En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = C_a \times I_a + I \times \left[ C_d \times (\vec{N} \cdot \vec{L}) + C_s \times (\vec{R} \cdot \vec{v}) \right]</math>
: Le terme géométrique est en réalité pris en compte implicitement pour le terme spéculaire, je ne rentre pas dan le détail.
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Avec tout ce qui a été dit précédemment, l'éclairage était calculé pour chaque sommet. Il attribuait une illumination/couleur à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation.
L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
La distinction de l'éclairage par pixel et par sommet est souvent complété avec une troisième possibilité : l''''éclairage par triangle'''. L'idée est de donner une couleur/illumination par triangle, et non par sommet. Les trois possibilités sont souvent appelées l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. A la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
La différence entre les trois algorithmes se voit assez facilement à lécran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calcul d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
qfjlnz8x8dxgq6njqq4fbws8c9aonfm
763389
763388
2026-04-10T14:20:24Z
Mewtow
31375
/* La réflexion de la lumière sur la surface */
763389
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D.
==Les bases du rendu 3D==
Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme.
===Les objets 3D et leur géométrie===
[[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]]
Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Un triangle est donc composé de 9 coordonnées, 3 par sommet.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets, pouvant contenir une certaine redondance ou des informations en plus. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie.
===La caméra : le point de vue depuis l'écran===
Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]]
La majorité des jeux vidéos ajoutent deux plans :
* un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches.
* Un ''far plane'', un'''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains.
Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels.
===Les textures===
Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]]
Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres.
Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique.
Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal.
La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''.
La rastérisation est structurée autour de trois étapes principales :
* une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D ;
* une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles ;
* une étape de '''rastérisation''' qui effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit.
==Le calcul de la géométrie==
Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective.
===Les trois étapes de transformation===
La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour.
De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]]
Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z).
[[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]]
Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces.
===Les changements de coordonnées se font via des multiplications de matrices===
Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc.
Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail.
Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble.
Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc.
Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps.
==L'élimination des surfaces cachées==
Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''.
Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre.
===Les différentes formes de ''culling''/''clipping''===
La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable.
[[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]]
Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]]
Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger.
L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z.
===L'algorithme du peintre===
Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal.
Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU.
Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment.
===Le tampon de profondeur===
Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]]
Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives.
Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective.
Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures.
===Le rendu en fil de fer===
[[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]]
Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]]
|}
Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet.
L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer.
Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres.
===Le rendu à primitives colorées===
[[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]]
Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]]
La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique.
Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés.
Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche.
Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse.
L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes.
Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''.
{|class="wikitable"
|-
! Géométrie
| Processeurs dédiés programmé pour émuler le pipeline graphique
|-
! Tri des quads du plus lointain au plus proche
| Processeur principal (implémentation logicielle)
|-
! Application des textures
| ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''.
|}
L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque.
Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn.
===Le placage de textures inverse===
Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet.
[[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]]
Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés.
[[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]]
Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé.
Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités.
L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes.
==L'éclairage d'une scène 3D==
L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante.
===La lumière incidente : le terme géométrique===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante.
La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
[[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.
[[File:Haze-Reflection-2.png|vignette|upright=1|Réflexion spéculaire.]]
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:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
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.
: <math>\text{BRDF spéculaire} = f(\vec{R} \cdot \vec{V}) </math>
La fonction en question peut être très compliquée, mais le cas classique est celui de la réflexion spéculaire de Phong. Un '''''Phong material''''' cumule à la fois une réflexion ambiante, une réflexion diffuse et une réflexion spéculaire. Il utilise la formule suivante, pour additionner les trois :
: <math>\text{Illumination totale} = C_a \times I_a + I \times \left[ C_d \times (\vec{N} \cdot \vec{L}) + C_s \times (\vec{R} \cdot \vec{v}) \right]</math>
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. Mais pour le reste, il est un peu bizarre. Déjà, le terme géométrique semble être absent, mais il est en réalité pris en compte. Il a juste disparu par une simplification de fraction dans le BRDF. Ensuite, il dépend du cosinus de l'angle entre R et V, mais élevé à une certaine puissance, dont l'exposant dépend du ''material'' considéré. La raison est qu'avec un cosinus simple, la lumière spéculaire ne serait pas atténuée assez vite quand on s'éloigne de la direction de réflexion maximale.
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Avec tout ce qui a été dit précédemment, l'éclairage était calculé pour chaque sommet. Il attribuait une illumination/couleur à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation.
L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
La distinction de l'éclairage par pixel et par sommet est souvent complété avec une troisième possibilité : l''''éclairage par triangle'''. L'idée est de donner une couleur/illumination par triangle, et non par sommet. Les trois possibilités sont souvent appelées l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. A la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
La différence entre les trois algorithmes se voit assez facilement à lécran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calcul d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
9e52h8nz6r711ot3xylhon8iqpikx6z
763390
763389
2026-04-10T14:21:44Z
Mewtow
31375
/* La réflexion de la lumière sur la surface */
763390
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D.
==Les bases du rendu 3D==
Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme.
===Les objets 3D et leur géométrie===
[[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]]
Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Un triangle est donc composé de 9 coordonnées, 3 par sommet.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets, pouvant contenir une certaine redondance ou des informations en plus. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie.
===La caméra : le point de vue depuis l'écran===
Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]]
La majorité des jeux vidéos ajoutent deux plans :
* un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches.
* Un ''far plane'', un'''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains.
Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels.
===Les textures===
Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]]
Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres.
Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique.
Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal.
La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''.
La rastérisation est structurée autour de trois étapes principales :
* une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D ;
* une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles ;
* une étape de '''rastérisation''' qui effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit.
==Le calcul de la géométrie==
Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective.
===Les trois étapes de transformation===
La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour.
De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]]
Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z).
[[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]]
Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces.
===Les changements de coordonnées se font via des multiplications de matrices===
Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc.
Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail.
Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble.
Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc.
Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps.
==L'élimination des surfaces cachées==
Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''.
Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre.
===Les différentes formes de ''culling''/''clipping''===
La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable.
[[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]]
Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]]
Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger.
L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z.
===L'algorithme du peintre===
Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal.
Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU.
Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment.
===Le tampon de profondeur===
Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]]
Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives.
Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective.
Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures.
===Le rendu en fil de fer===
[[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]]
Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]]
|}
Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet.
L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer.
Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres.
===Le rendu à primitives colorées===
[[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]]
Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]]
La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique.
Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés.
Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche.
Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse.
L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes.
Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''.
{|class="wikitable"
|-
! Géométrie
| Processeurs dédiés programmé pour émuler le pipeline graphique
|-
! Tri des quads du plus lointain au plus proche
| Processeur principal (implémentation logicielle)
|-
! Application des textures
| ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''.
|}
L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque.
Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn.
===Le placage de textures inverse===
Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet.
[[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]]
Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés.
[[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]]
Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé.
Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités.
L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes.
==L'éclairage d'une scène 3D==
L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante.
===La lumière incidente : le terme géométrique===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante.
La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
[[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.
[[File:Haze-Reflection-2.png|vignette|upright=1|Réflexion spéculaire.]]
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:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
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 réflexion ambiante, spéculaire et diffuse sont additionnées entre elles pour donner l'éclairage final.
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Avec tout ce qui a été dit précédemment, l'éclairage était calculé pour chaque sommet. Il attribuait une illumination/couleur à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation.
L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
La distinction de l'éclairage par pixel et par sommet est souvent complété avec une troisième possibilité : l''''éclairage par triangle'''. L'idée est de donner une couleur/illumination par triangle, et non par sommet. Les trois possibilités sont souvent appelées l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. A la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
La différence entre les trois algorithmes se voit assez facilement à lécran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calcul d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
o4hw48ilmivmxjdqccrmci2jdddrzc3
763391
763390
2026-04-10T14:22:50Z
Mewtow
31375
/* La réflexion de la lumière sur la surface */
763391
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D.
==Les bases du rendu 3D==
Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme.
===Les objets 3D et leur géométrie===
[[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]]
Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Un triangle est donc composé de 9 coordonnées, 3 par sommet.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets, pouvant contenir une certaine redondance ou des informations en plus. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie.
===La caméra : le point de vue depuis l'écran===
Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]]
La majorité des jeux vidéos ajoutent deux plans :
* un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches.
* Un ''far plane'', un'''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains.
Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels.
===Les textures===
Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]]
Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres.
Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique.
Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal.
La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''.
La rastérisation est structurée autour de trois étapes principales :
* une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D ;
* une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles ;
* une étape de '''rastérisation''' qui effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit.
==Le calcul de la géométrie==
Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective.
===Les trois étapes de transformation===
La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour.
De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]]
Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z).
[[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]]
Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces.
===Les changements de coordonnées se font via des multiplications de matrices===
Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc.
Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail.
Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble.
Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc.
Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps.
==L'élimination des surfaces cachées==
Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''.
Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre.
===Les différentes formes de ''culling''/''clipping''===
La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable.
[[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]]
Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]]
Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger.
L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z.
===L'algorithme du peintre===
Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal.
Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU.
Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment.
===Le tampon de profondeur===
Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]]
Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives.
Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective.
Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures.
===Le rendu en fil de fer===
[[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]]
Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]]
|}
Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet.
L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer.
Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres.
===Le rendu à primitives colorées===
[[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]]
Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]]
La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique.
Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés.
Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche.
Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse.
L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes.
Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''.
{|class="wikitable"
|-
! Géométrie
| Processeurs dédiés programmé pour émuler le pipeline graphique
|-
! Tri des quads du plus lointain au plus proche
| Processeur principal (implémentation logicielle)
|-
! Application des textures
| ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''.
|}
L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque.
Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn.
===Le placage de textures inverse===
Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet.
[[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]]
Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés.
[[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]]
Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé.
Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités.
L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes.
==L'éclairage d'une scène 3D==
L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante.
===La lumière incidente : le terme géométrique===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante.
La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
[[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.
[[File:Haze-Reflection-2.png|vignette|upright=1|Réflexion spéculaire.]]
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:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
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.
La réflexion ambiante, spéculaire et diffuse sont additionnées entre elles pour donner l'éclairage final.
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Avec tout ce qui a été dit précédemment, l'éclairage était calculé pour chaque sommet. Il attribuait une illumination/couleur à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation.
L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
La distinction de l'éclairage par pixel et par sommet est souvent complété avec une troisième possibilité : l''''éclairage par triangle'''. L'idée est de donner une couleur/illumination par triangle, et non par sommet. Les trois possibilités sont souvent appelées l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. A la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
La différence entre les trois algorithmes se voit assez facilement à lécran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calcul d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
j6l6wy5f84lm1rsb3uua7xqu0ycyxjj
763392
763391
2026-04-10T14:23:32Z
Mewtow
31375
/* La réflexion de la lumière sur la surface */
763392
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D.
==Les bases du rendu 3D==
Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme.
===Les objets 3D et leur géométrie===
[[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]]
Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Un triangle est donc composé de 9 coordonnées, 3 par sommet.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets, pouvant contenir une certaine redondance ou des informations en plus. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie.
===La caméra : le point de vue depuis l'écran===
Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]]
La majorité des jeux vidéos ajoutent deux plans :
* un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches.
* Un ''far plane'', un'''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains.
Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels.
===Les textures===
Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]]
Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres.
Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique.
Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal.
La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''.
La rastérisation est structurée autour de trois étapes principales :
* une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D ;
* une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles ;
* une étape de '''rastérisation''' qui effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit.
==Le calcul de la géométrie==
Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective.
===Les trois étapes de transformation===
La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour.
De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]]
Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z).
[[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]]
Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces.
===Les changements de coordonnées se font via des multiplications de matrices===
Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc.
Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail.
Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble.
Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc.
Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps.
==L'élimination des surfaces cachées==
Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''.
Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre.
===Les différentes formes de ''culling''/''clipping''===
La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable.
[[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]]
Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]]
Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger.
L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z.
===L'algorithme du peintre===
Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal.
Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU.
Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment.
===Le tampon de profondeur===
Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]]
Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives.
Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective.
Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures.
===Le rendu en fil de fer===
[[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]]
Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]]
|}
Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet.
L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer.
Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres.
===Le rendu à primitives colorées===
[[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]]
Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]]
La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique.
Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés.
Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche.
Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse.
L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes.
Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''.
{|class="wikitable"
|-
! Géométrie
| Processeurs dédiés programmé pour émuler le pipeline graphique
|-
! Tri des quads du plus lointain au plus proche
| Processeur principal (implémentation logicielle)
|-
! Application des textures
| ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''.
|}
L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque.
Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn.
===Le placage de textures inverse===
Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet.
[[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]]
Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés.
[[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]]
Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé.
Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités.
L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes.
==L'éclairage d'une scène 3D==
L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante.
===La lumière incidente : le terme géométrique===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante.
La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
[[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:Haze-Reflection-2.png|centre|vignette|upright=1|Réflexion spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
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.
La réflexion ambiante, spéculaire et diffuse sont additionnées entre elles pour donner l'éclairage final.
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Avec tout ce qui a été dit précédemment, l'éclairage était calculé pour chaque sommet. Il attribuait une illumination/couleur à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation.
L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
La distinction de l'éclairage par pixel et par sommet est souvent complété avec une troisième possibilité : l''''éclairage par triangle'''. L'idée est de donner une couleur/illumination par triangle, et non par sommet. Les trois possibilités sont souvent appelées l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. A la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
La différence entre les trois algorithmes se voit assez facilement à lécran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calcul d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
jfldjbm7t5oqxtsiqedjim91zwyns7u
763393
763392
2026-04-10T14:24:01Z
Mewtow
31375
/* La réflexion de la lumière sur la surface */
763393
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D.
==Les bases du rendu 3D==
Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme.
===Les objets 3D et leur géométrie===
[[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]]
Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Un triangle est donc composé de 9 coordonnées, 3 par sommet.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets, pouvant contenir une certaine redondance ou des informations en plus. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie.
===La caméra : le point de vue depuis l'écran===
Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]]
La majorité des jeux vidéos ajoutent deux plans :
* un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches.
* Un ''far plane'', un'''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains.
Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels.
===Les textures===
Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]]
Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres.
Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique.
Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal.
La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''.
La rastérisation est structurée autour de trois étapes principales :
* une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D ;
* une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles ;
* une étape de '''rastérisation''' qui effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit.
==Le calcul de la géométrie==
Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective.
===Les trois étapes de transformation===
La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour.
De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]]
Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z).
[[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]]
Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces.
===Les changements de coordonnées se font via des multiplications de matrices===
Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc.
Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail.
Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble.
Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc.
Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps.
==L'élimination des surfaces cachées==
Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''.
Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre.
===Les différentes formes de ''culling''/''clipping''===
La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable.
[[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]]
Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]]
Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger.
L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z.
===L'algorithme du peintre===
Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal.
Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU.
Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment.
===Le tampon de profondeur===
Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]]
Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives.
Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective.
Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures.
===Le rendu en fil de fer===
[[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]]
Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]]
|}
Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet.
L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer.
Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres.
===Le rendu à primitives colorées===
[[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]]
Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]]
La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique.
Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés.
Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche.
Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse.
L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes.
Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''.
{|class="wikitable"
|-
! Géométrie
| Processeurs dédiés programmé pour émuler le pipeline graphique
|-
! Tri des quads du plus lointain au plus proche
| Processeur principal (implémentation logicielle)
|-
! Application des textures
| ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''.
|}
L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque.
Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn.
===Le placage de textures inverse===
Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet.
[[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]]
Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés.
[[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]]
Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé.
Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités.
L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes.
==L'éclairage d'une scène 3D==
L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante.
===La lumière incidente : le terme géométrique===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante.
La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
[[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 algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Avec tout ce qui a été dit précédemment, l'éclairage était calculé pour chaque sommet. Il attribuait une illumination/couleur à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation.
L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
La distinction de l'éclairage par pixel et par sommet est souvent complété avec une troisième possibilité : l''''éclairage par triangle'''. L'idée est de donner une couleur/illumination par triangle, et non par sommet. Les trois possibilités sont souvent appelées l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. A la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
La différence entre les trois algorithmes se voit assez facilement à lécran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calcul d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
fnv72fktsu8cox03w5vacpremylyqhv
763394
763393
2026-04-10T14:25:49Z
Mewtow
31375
/* La réflexion de la lumière sur la surface */
763394
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D.
==Les bases du rendu 3D==
Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme.
===Les objets 3D et leur géométrie===
[[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]]
Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Un triangle est donc composé de 9 coordonnées, 3 par sommet.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets, pouvant contenir une certaine redondance ou des informations en plus. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie.
===La caméra : le point de vue depuis l'écran===
Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]]
La majorité des jeux vidéos ajoutent deux plans :
* un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches.
* Un ''far plane'', un'''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains.
Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels.
===Les textures===
Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]]
Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres.
Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique.
Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal.
La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''.
La rastérisation est structurée autour de trois étapes principales :
* une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D ;
* une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles ;
* une étape de '''rastérisation''' qui effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit.
==Le calcul de la géométrie==
Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective.
===Les trois étapes de transformation===
La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour.
De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]]
Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z).
[[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]]
Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces.
===Les changements de coordonnées se font via des multiplications de matrices===
Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc.
Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail.
Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble.
Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc.
Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps.
==L'élimination des surfaces cachées==
Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''.
Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre.
===Les différentes formes de ''culling''/''clipping''===
La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable.
[[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]]
Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]]
Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger.
L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z.
===L'algorithme du peintre===
Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal.
Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU.
Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment.
===Le tampon de profondeur===
Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]]
Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives.
Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective.
Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures.
===Le rendu en fil de fer===
[[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]]
Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]]
|}
Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet.
L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer.
Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres.
===Le rendu à primitives colorées===
[[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]]
Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]]
La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique.
Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés.
Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche.
Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse.
L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes.
Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''.
{|class="wikitable"
|-
! Géométrie
| Processeurs dédiés programmé pour émuler le pipeline graphique
|-
! Tri des quads du plus lointain au plus proche
| Processeur principal (implémentation logicielle)
|-
! Application des textures
| ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''.
|}
L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque.
Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn.
===Le placage de textures inverse===
Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet.
[[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]]
Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés.
[[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]]
Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé.
Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités.
L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes.
==L'éclairage d'une scène 3D==
L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante.
===La lumière incidente : le terme géométrique===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante.
La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
[[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===
Avec tout ce qui a été dit précédemment, l'éclairage était calculé pour chaque sommet. Il attribuait une illumination/couleur à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation.
L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
La distinction de l'éclairage par pixel et par sommet est souvent complété avec une troisième possibilité : l''''éclairage par triangle'''. L'idée est de donner une couleur/illumination par triangle, et non par sommet. Les trois possibilités sont souvent appelées l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. A la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
La différence entre les trois algorithmes se voit assez facilement à lécran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calcul d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
64e7mcq5r05d9pb6xbf4og9gbx0tgut
763395
763394
2026-04-10T14:36:10Z
Mewtow
31375
/* Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel */
763395
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D.
==Les bases du rendu 3D==
Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme.
===Les objets 3D et leur géométrie===
[[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]]
Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Un triangle est donc composé de 9 coordonnées, 3 par sommet.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets, pouvant contenir une certaine redondance ou des informations en plus. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie.
===La caméra : le point de vue depuis l'écran===
Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]]
La majorité des jeux vidéos ajoutent deux plans :
* un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches.
* Un ''far plane'', un'''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains.
Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels.
===Les textures===
Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]]
Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres.
Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique.
Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal.
La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''.
La rastérisation est structurée autour de trois étapes principales :
* une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D ;
* une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles ;
* une étape de '''rastérisation''' qui effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit.
==Le calcul de la géométrie==
Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective.
===Les trois étapes de transformation===
La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour.
De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]]
Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z).
[[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]]
Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces.
===Les changements de coordonnées se font via des multiplications de matrices===
Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc.
Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail.
Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble.
Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc.
Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps.
==L'élimination des surfaces cachées==
Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''.
Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre.
===Les différentes formes de ''culling''/''clipping''===
La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable.
[[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]]
Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]]
Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger.
L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z.
===L'algorithme du peintre===
Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal.
Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU.
Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment.
===Le tampon de profondeur===
Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]]
Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives.
Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective.
Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures.
===Le rendu en fil de fer===
[[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]]
Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]]
|}
Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet.
L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer.
Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres.
===Le rendu à primitives colorées===
[[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]]
Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]]
La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique.
Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés.
Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche.
Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse.
L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes.
Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''.
{|class="wikitable"
|-
! Géométrie
| Processeurs dédiés programmé pour émuler le pipeline graphique
|-
! Tri des quads du plus lointain au plus proche
| Processeur principal (implémentation logicielle)
|-
! Application des textures
| ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''.
|}
L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque.
Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn.
===Le placage de textures inverse===
Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet.
[[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]]
Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés.
[[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]]
Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé.
Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités.
L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes.
==L'éclairage d'une scène 3D==
L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante.
===La lumière incidente : le terme géométrique===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante.
La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
[[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 ''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.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
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 cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calcul d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
gdf4ltb7zrbiwdo6ox523vmcclukf53
763396
763395
2026-04-10T14:36:19Z
Mewtow
31375
/* Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel */
763396
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D.
==Les bases du rendu 3D==
Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme.
===Les objets 3D et leur géométrie===
[[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]]
Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Un triangle est donc composé de 9 coordonnées, 3 par sommet.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets, pouvant contenir une certaine redondance ou des informations en plus. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie.
===La caméra : le point de vue depuis l'écran===
Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]]
La majorité des jeux vidéos ajoutent deux plans :
* un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches.
* Un ''far plane'', un'''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains.
Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels.
===Les textures===
Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]]
Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres.
Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique.
Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal.
La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''.
La rastérisation est structurée autour de trois étapes principales :
* une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D ;
* une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles ;
* une étape de '''rastérisation''' qui effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit.
==Le calcul de la géométrie==
Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective.
===Les trois étapes de transformation===
La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour.
De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]]
Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z).
[[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]]
Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces.
===Les changements de coordonnées se font via des multiplications de matrices===
Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc.
Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail.
Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble.
Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc.
Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps.
==L'élimination des surfaces cachées==
Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''.
Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre.
===Les différentes formes de ''culling''/''clipping''===
La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable.
[[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]]
Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]]
Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger.
L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z.
===L'algorithme du peintre===
Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal.
Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU.
Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment.
===Le tampon de profondeur===
Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]]
Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives.
Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective.
Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures.
===Le rendu en fil de fer===
[[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]]
Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]]
|}
Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet.
L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer.
Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres.
===Le rendu à primitives colorées===
[[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]]
Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]]
La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique.
Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés.
Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche.
Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse.
L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes.
Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''.
{|class="wikitable"
|-
! Géométrie
| Processeurs dédiés programmé pour émuler le pipeline graphique
|-
! Tri des quads du plus lointain au plus proche
| Processeur principal (implémentation logicielle)
|-
! Application des textures
| ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''.
|}
L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque.
Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn.
===Le placage de textures inverse===
Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet.
[[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]]
Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés.
[[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]]
Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé.
Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités.
L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes.
==L'éclairage d'une scène 3D==
L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante.
===La lumière incidente : le terme géométrique===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante.
La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
[[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 ''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.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
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 cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calcul d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
g7p2sd592kwoeh7fvuu9gqepfyjqazu
763397
763396
2026-04-10T14:36:30Z
Mewtow
31375
/* Le bump-mapping et autres approximations de l'éclairage par pixel */
763397
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D.
==Les bases du rendu 3D==
Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme.
===Les objets 3D et leur géométrie===
[[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]]
Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Un triangle est donc composé de 9 coordonnées, 3 par sommet.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets, pouvant contenir une certaine redondance ou des informations en plus. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie.
===La caméra : le point de vue depuis l'écran===
Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]]
La majorité des jeux vidéos ajoutent deux plans :
* un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches.
* Un ''far plane'', un'''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains.
Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels.
===Les textures===
Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]]
Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres.
Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique.
Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal.
La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''.
La rastérisation est structurée autour de trois étapes principales :
* une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D ;
* une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles ;
* une étape de '''rastérisation''' qui effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit.
==Le calcul de la géométrie==
Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective.
===Les trois étapes de transformation===
La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour.
De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]]
Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z).
[[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]]
Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces.
===Les changements de coordonnées se font via des multiplications de matrices===
Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc.
Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail.
Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble.
Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc.
Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps.
==L'élimination des surfaces cachées==
Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''.
Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre.
===Les différentes formes de ''culling''/''clipping''===
La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable.
[[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]]
Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]]
Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger.
L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z.
===L'algorithme du peintre===
Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal.
Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU.
Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment.
===Le tampon de profondeur===
Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]]
Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives.
Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective.
Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures.
===Le rendu en fil de fer===
[[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]]
Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]]
|}
Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet.
L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer.
Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres.
===Le rendu à primitives colorées===
[[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]]
Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]]
La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique.
Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés.
Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche.
Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse.
L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes.
Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''.
{|class="wikitable"
|-
! Géométrie
| Processeurs dédiés programmé pour émuler le pipeline graphique
|-
! Tri des quads du plus lointain au plus proche
| Processeur principal (implémentation logicielle)
|-
! Application des textures
| ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''.
|}
L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque.
Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn.
===Le placage de textures inverse===
Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet.
[[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]]
Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés.
[[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]]
Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé.
Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités.
L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes.
==L'éclairage d'une scène 3D==
L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante.
===La lumière incidente : le terme géométrique===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante.
La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
[[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 ''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.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
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 cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
7pgtk73ujvur08rksgk5rrwtgenlirs
763398
763397
2026-04-10T14:40:39Z
Mewtow
31375
/* Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel */
763398
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D.
==Les bases du rendu 3D==
Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme.
===Les objets 3D et leur géométrie===
[[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]]
Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Un triangle est donc composé de 9 coordonnées, 3 par sommet.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets, pouvant contenir une certaine redondance ou des informations en plus. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie.
===La caméra : le point de vue depuis l'écran===
Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]]
La majorité des jeux vidéos ajoutent deux plans :
* un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches.
* Un ''far plane'', un'''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains.
Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels.
===Les textures===
Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]]
Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres.
Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique.
Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal.
La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''.
La rastérisation est structurée autour de trois étapes principales :
* une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D ;
* une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles ;
* une étape de '''rastérisation''' qui effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit.
==Le calcul de la géométrie==
Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective.
===Les trois étapes de transformation===
La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour.
De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]]
Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z).
[[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]]
Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces.
===Les changements de coordonnées se font via des multiplications de matrices===
Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc.
Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail.
Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble.
Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc.
Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps.
==L'élimination des surfaces cachées==
Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''.
Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre.
===Les différentes formes de ''culling''/''clipping''===
La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable.
[[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]]
Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]]
Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger.
L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z.
===L'algorithme du peintre===
Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal.
Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU.
Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment.
===Le tampon de profondeur===
Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]]
Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives.
Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective.
Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures.
===Le rendu en fil de fer===
[[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]]
Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]]
|}
Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet.
L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer.
Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres.
===Le rendu à primitives colorées===
[[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]]
Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]]
La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique.
Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés.
Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche.
Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse.
L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes.
Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''.
{|class="wikitable"
|-
! Géométrie
| Processeurs dédiés programmé pour émuler le pipeline graphique
|-
! Tri des quads du plus lointain au plus proche
| Processeur principal (implémentation logicielle)
|-
! Application des textures
| ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''.
|}
L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque.
Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn.
===Le placage de textures inverse===
Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet.
[[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]]
Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés.
[[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]]
Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé.
Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités.
L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes.
==L'éclairage d'une scène 3D==
L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante.
===La lumière incidente : le terme géométrique===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante.
La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
[[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.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec.
Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet. 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]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-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 cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
ku12j1slc4kgm34tfg48ghxt6kfytvy
763399
763398
2026-04-10T14:41:23Z
Mewtow
31375
/* Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel */
763399
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D.
==Les bases du rendu 3D==
Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme.
===Les objets 3D et leur géométrie===
[[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]]
Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Un triangle est donc composé de 9 coordonnées, 3 par sommet.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets, pouvant contenir une certaine redondance ou des informations en plus. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie.
===La caméra : le point de vue depuis l'écran===
Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]]
La majorité des jeux vidéos ajoutent deux plans :
* un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches.
* Un ''far plane'', un'''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains.
Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels.
===Les textures===
Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]]
Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres.
Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique.
Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal.
La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''.
La rastérisation est structurée autour de trois étapes principales :
* une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D ;
* une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles ;
* une étape de '''rastérisation''' qui effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit.
==Le calcul de la géométrie==
Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective.
===Les trois étapes de transformation===
La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour.
De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]]
Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z).
[[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]]
Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces.
===Les changements de coordonnées se font via des multiplications de matrices===
Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc.
Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail.
Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble.
Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc.
Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps.
==L'élimination des surfaces cachées==
Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''.
Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre.
===Les différentes formes de ''culling''/''clipping''===
La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable.
[[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]]
Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]]
Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger.
L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z.
===L'algorithme du peintre===
Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal.
Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU.
Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment.
===Le tampon de profondeur===
Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]]
Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives.
Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective.
Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures.
===Le rendu en fil de fer===
[[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]]
Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]]
|}
Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet.
L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer.
Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres.
===Le rendu à primitives colorées===
[[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]]
Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]]
La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique.
Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés.
Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche.
Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse.
L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes.
Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''.
{|class="wikitable"
|-
! Géométrie
| Processeurs dédiés programmé pour émuler le pipeline graphique
|-
! Tri des quads du plus lointain au plus proche
| Processeur principal (implémentation logicielle)
|-
! Application des textures
| ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''.
|}
L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque.
Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn.
===Le placage de textures inverse===
Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet.
[[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]]
Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés.
[[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]]
Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé.
Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités.
L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes.
==L'éclairage d'une scène 3D==
L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante.
===La lumière incidente : le terme géométrique===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante.
La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
[[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.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec.
Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet. 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]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-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. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
ptldy1kdcj7vjvn9z7guoacke0au3rb
763401
763399
2026-04-10T16:23:05Z
Mewtow
31375
/* Les shaders */
763401
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D.
==Les bases du rendu 3D==
Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme.
===Les objets 3D et leur géométrie===
[[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]]
Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Un triangle est donc composé de 9 coordonnées, 3 par sommet.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets, pouvant contenir une certaine redondance ou des informations en plus. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie.
===La caméra : le point de vue depuis l'écran===
Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]]
La majorité des jeux vidéos ajoutent deux plans :
* un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches.
* Un ''far plane'', un'''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains.
Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels.
===Les textures===
Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]]
Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres.
Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique.
Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal.
La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''.
La rastérisation est structurée autour de trois étapes principales :
* une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D ;
* une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles ;
* une étape de '''rastérisation''' qui effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit.
==Le calcul de la géométrie==
Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective.
===Les trois étapes de transformation===
La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour.
De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]]
Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z).
[[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]]
Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces.
===Les changements de coordonnées se font via des multiplications de matrices===
Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc.
Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail.
Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble.
Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc.
Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps.
==L'élimination des surfaces cachées==
Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''.
Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre.
===Les différentes formes de ''culling''/''clipping''===
La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable.
[[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]]
Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]]
Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger.
L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z.
===L'algorithme du peintre===
Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal.
Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU.
Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment.
===Le tampon de profondeur===
Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]]
Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives.
Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective.
Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures.
===Le rendu en fil de fer===
[[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]]
Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]]
|}
Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet.
L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer.
Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres.
===Le rendu à primitives colorées===
[[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]]
Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]]
La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique.
Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés.
Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche.
Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse.
L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes.
Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''.
{|class="wikitable"
|-
! Géométrie
| Processeurs dédiés programmé pour émuler le pipeline graphique
|-
! Tri des quads du plus lointain au plus proche
| Processeur principal (implémentation logicielle)
|-
! Application des textures
| ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''.
|}
L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque.
Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn.
===Le placage de textures inverse===
Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet.
[[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]]
Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés.
[[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]]
Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé.
Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités.
L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes.
==L'éclairage d'une scène 3D==
L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante.
===La lumière incidente : le terme géométrique===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante.
La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
[[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.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec.
Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet. 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]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-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=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
9ojy73dhb8fq867t0z3jo2sbwzaqdim
763402
763401
2026-04-10T16:30:33Z
Mewtow
31375
/* Les objets 3D et leur géométrie */
763402
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.
Une paire de sommets connectée par un segment s'appelle une arête, comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, appelée une face, ou encore une '''primitive'''. Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Aujourd'hui, les API 3D ne gèrent plus le rendu avec des ''quads'', qui est tombé en désuétude. Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets, pouvant contenir une certaine redondance ou des informations en plus. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie.
===La caméra : le point de vue depuis l'écran===
Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]]
La majorité des jeux vidéos ajoutent deux plans :
* un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches.
* Un ''far plane'', un'''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains.
Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels.
===Les textures===
Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]]
Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres.
Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique.
Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal.
La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''.
La rastérisation est structurée autour de trois étapes principales :
* une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D ;
* une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles ;
* une étape de '''rastérisation''' qui effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit.
==Le calcul de la géométrie==
Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective.
===Les trois étapes de transformation===
La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour.
De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]]
Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z).
[[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]]
Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces.
===Les changements de coordonnées se font via des multiplications de matrices===
Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc.
Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail.
Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble.
Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc.
Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps.
==L'élimination des surfaces cachées==
Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''.
Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre.
===Les différentes formes de ''culling''/''clipping''===
La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable.
[[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]]
Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]]
Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger.
L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z.
===L'algorithme du peintre===
Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal.
Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU.
Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment.
===Le tampon de profondeur===
Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]]
Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives.
Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective.
Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures.
===Le rendu en fil de fer===
[[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]]
Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]]
|}
Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet.
L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer.
Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres.
===Le rendu à primitives colorées===
[[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]]
Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]]
La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique.
Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés.
Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche.
Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse.
L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes.
Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''.
{|class="wikitable"
|-
! Géométrie
| Processeurs dédiés programmé pour émuler le pipeline graphique
|-
! Tri des quads du plus lointain au plus proche
| Processeur principal (implémentation logicielle)
|-
! Application des textures
| ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''.
|}
L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque.
Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn.
===Le placage de textures inverse===
Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet.
[[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]]
Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés.
[[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]]
Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé.
Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités.
L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes.
==L'éclairage d'une scène 3D==
L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante.
===La lumière incidente : le terme géométrique===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante.
La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
[[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.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec.
Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet. 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]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-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=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
cjz4bm4fkyj61ykbvaumc9bl6b6fepj
763403
763402
2026-04-10T16:31:11Z
Mewtow
31375
/* Les objets 3D et leur géométrie */
763403
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.
Une paire de sommets connectée par un segment s'appelle une arête, comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, appelée une face, ou encore une '''primitive'''. Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Les quadrilatères étaient autrefois utilisés, mais ils sont tombés en désuétude. Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets, pouvant contenir une certaine redondance ou des informations en plus. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie.
===La caméra : le point de vue depuis l'écran===
Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]]
La majorité des jeux vidéos ajoutent deux plans :
* un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches.
* Un ''far plane'', un'''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains.
Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels.
===Les textures===
Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]]
Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres.
Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique.
Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal.
La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''.
La rastérisation est structurée autour de trois étapes principales :
* une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D ;
* une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles ;
* une étape de '''rastérisation''' qui effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit.
==Le calcul de la géométrie==
Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective.
===Les trois étapes de transformation===
La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour.
De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]]
Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z).
[[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]]
Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces.
===Les changements de coordonnées se font via des multiplications de matrices===
Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc.
Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail.
Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble.
Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc.
Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps.
==L'élimination des surfaces cachées==
Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''.
Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre.
===Les différentes formes de ''culling''/''clipping''===
La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable.
[[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]]
Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]]
Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger.
L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z.
===L'algorithme du peintre===
Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal.
Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU.
Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment.
===Le tampon de profondeur===
Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]]
Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives.
Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective.
Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures.
===Le rendu en fil de fer===
[[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]]
Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]]
|}
Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet.
L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer.
Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres.
===Le rendu à primitives colorées===
[[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]]
Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]]
La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique.
Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés.
Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche.
Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse.
L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes.
Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''.
{|class="wikitable"
|-
! Géométrie
| Processeurs dédiés programmé pour émuler le pipeline graphique
|-
! Tri des quads du plus lointain au plus proche
| Processeur principal (implémentation logicielle)
|-
! Application des textures
| ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''.
|}
L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque.
Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn.
===Le placage de textures inverse===
Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet.
[[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]]
Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale.
[[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]]
Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés.
[[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]]
Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé.
Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités.
L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes.
==L'éclairage d'une scène 3D==
L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante.
===La lumière incidente : le terme géométrique===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante.
La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
[[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.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec.
Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet. 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]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-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=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
jbug0rl685abpk3ehwng0idsdkd6tpw
763404
763403
2026-04-10T16:35:25Z
Mewtow
31375
/* Les objets 3D et leur géométrie */
763404
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.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec.
Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet. 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]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-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=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
s819v7nv3udkmatsc753qzu8ufpkuyv
763425
763404
2026-04-10T19:39:35Z
Mewtow
31375
/* Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel */
763425
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=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
9kj2jochrnctwwowt8bdn403m9fid5y
Les cartes graphiques/Le pipeline géométrique d'un GPU
0
79241
763426
759598
2026-04-10T19:53:45Z
Mewtow
31375
/* DirectX 10 : les geometry shaders */
763426
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons étudié la manière dont les anciennes cartes graphiques traitaient la géométrie. Elles traitaient uniquement des sommets, via des ''vertex shaders''. Mais depuis DirectX 10, le pipeline graphique a intégré des techniques pour gérer nativement des triangles et faire des traitements dessus. 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.
Contrairement à l'ancien pipeline graphique, le nouveau pipeline graphique gère nativement des primitives. Pour rappel, les primitives sont tout simplement les triangles ou les polygones utilisés pour décrire les modèles 3D, les surfaces de la scène 3D. Les moteurs de rendu acceptent aussi des primitives simples, comme des points (utiles pour les particules), ou les lignes (utiles pour le rendu 2D). Les primitives sont toutes définies par un ou plusieurs points : trois sommets pour un triangle.
Dans l'ancien pipeline graphique, les primitives sont assemblées dans la dernière étape géométrique, avant le rastériseur. Aucun traitement n'est effectué sur les primitives, qui sont juste envoyées au rastériseur. Elles sont éventuellement éliminée via ''culling'', mais c'est le rastériseur qui s'en charge. Tout traitement géométrique est réalisé en manipulant des sommets, via un ''vertex shader''. mais cette organisation est rapidement devenue impraticable. Elle empêchait certaines optimisations, notamment l'élimination précoce des primitives invisibles : il fallait attendre la rastérisation pour les éliminer, elles étaient transformées et éclairées même si elles étaient invisibles. De plus, quelques fonctionnalités graphiques étaient impossibles. Voyons l'une d'entre elle : la tesselation.
==Un exemple d'utilisation des primitives : la tessellation==
La '''tessellation''' est une technique qui permet d'ajouter des primitives à 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 nouvelles primitives.
{|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.
==DirectX 10 : les ''geometry shaders''==
DirectX 10 et OpenGl 3.2 ont introduit les ''geometry shaders'', juste avant l'étape d'assemblage des primitives. Ils sont surtout utilisés pour la gestion des ''cubemaps'', le ''shadow volume extrusion'', la génération de particules, et quelques autres effets graphiques. Ils pourraient en théorie ê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.
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.
Un point important est que 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'').
===L'étape d’assemblage de primitives est dupliquée===
Les ''geometry shaders'' sont exécutés par les processeurs de shaders normaux. 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>
o62wispn6guetz0g4pvwmiyimfyjno5
763427
763426
2026-04-10T19:54:28Z
Mewtow
31375
/* DirectX 10 : les geometry shaders */
763427
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons étudié la manière dont les anciennes cartes graphiques traitaient la géométrie. Elles traitaient uniquement des sommets, via des ''vertex shaders''. Mais depuis DirectX 10, le pipeline graphique a intégré des techniques pour gérer nativement des triangles et faire des traitements dessus. 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.
Contrairement à l'ancien pipeline graphique, le nouveau pipeline graphique gère nativement des primitives. Pour rappel, les primitives sont tout simplement les triangles ou les polygones utilisés pour décrire les modèles 3D, les surfaces de la scène 3D. Les moteurs de rendu acceptent aussi des primitives simples, comme des points (utiles pour les particules), ou les lignes (utiles pour le rendu 2D). Les primitives sont toutes définies par un ou plusieurs points : trois sommets pour un triangle.
Dans l'ancien pipeline graphique, les primitives sont assemblées dans la dernière étape géométrique, avant le rastériseur. Aucun traitement n'est effectué sur les primitives, qui sont juste envoyées au rastériseur. Elles sont éventuellement éliminée via ''culling'', mais c'est le rastériseur qui s'en charge. Tout traitement géométrique est réalisé en manipulant des sommets, via un ''vertex shader''. mais cette organisation est rapidement devenue impraticable. Elle empêchait certaines optimisations, notamment l'élimination précoce des primitives invisibles : il fallait attendre la rastérisation pour les éliminer, elles étaient transformées et éclairées même si elles étaient invisibles. De plus, quelques fonctionnalités graphiques étaient impossibles. Voyons l'une d'entre elle : la tesselation.
==Un exemple d'utilisation des primitives : la tessellation==
La '''tessellation''' est une technique qui permet d'ajouter des primitives à 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 nouvelles primitives.
{|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.
==DirectX 10 : les ''geometry shaders''==
DirectX 10 et OpenGl 3.2 ont introduit les ''geometry shaders'', juste avant l'étape d'assemblage des primitives. Ils sont surtout utilisés pour la gestion des ''cubemaps'', le ''shadow volume extrusion'', la génération de particules, et quelques autres effets graphiques. Ils pourraient en théorie ê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.
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.
===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>
9ymimrgad57hwxya279ll4fagccno01
763428
763427
2026-04-10T20:05:03Z
Mewtow
31375
/* DirectX 10 : les geometry shaders */
763428
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons étudié la manière dont les anciennes cartes graphiques traitaient la géométrie. Elles traitaient uniquement des sommets, via des ''vertex shaders''. Mais depuis DirectX 10, le pipeline graphique a intégré des techniques pour gérer nativement des triangles et faire des traitements dessus. 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.
Contrairement à l'ancien pipeline graphique, le nouveau pipeline graphique gère nativement des primitives. Pour rappel, les primitives sont tout simplement les triangles ou les polygones utilisés pour décrire les modèles 3D, les surfaces de la scène 3D. Les moteurs de rendu acceptent aussi des primitives simples, comme des points (utiles pour les particules), ou les lignes (utiles pour le rendu 2D). Les primitives sont toutes définies par un ou plusieurs points : trois sommets pour un triangle.
Dans l'ancien pipeline graphique, les primitives sont assemblées dans la dernière étape géométrique, avant le rastériseur. Aucun traitement n'est effectué sur les primitives, qui sont juste envoyées au rastériseur. Elles sont éventuellement éliminée via ''culling'', mais c'est le rastériseur qui s'en charge. Tout traitement géométrique est réalisé en manipulant des sommets, via un ''vertex shader''. mais cette organisation est rapidement devenue impraticable. Elle empêchait certaines optimisations, notamment l'élimination précoce des primitives invisibles : il fallait attendre la rastérisation pour les éliminer, elles étaient transformées et éclairées même si elles étaient invisibles. De plus, quelques fonctionnalités graphiques étaient impossibles. Voyons l'une d'entre elle : la tesselation.
==Un exemple d'utilisation des primitives : la tessellation==
La '''tessellation''' est une technique qui permet d'ajouter des primitives à 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 nouvelles primitives.
{|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.
==DirectX 10 : les ''geometry shaders''==
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>
00mhhtc0hg0m31ms25hgymeqdsen7ji
763429
763428
2026-04-10T20:10:39Z
Mewtow
31375
763429
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons étudié 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''. 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. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12.
: Pour rappel, les primitives sont tout simplement les triangles ou les polygones utilisés pour décrire les modèles 3D, les surfaces de la scène 3D. Les moteurs de rendu acceptent aussi des primitives simples, comme des points (utiles pour les particules), ou les lignes (utiles pour le rendu 2D). Les primitives sont toutes définies par un ou plusieurs points : trois sommets pour un triangle.
==Un exemple d'utilisation des primitives : la tessellation==
La '''tessellation''' est une technique qui permet d'ajouter des primitives à 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 nouvelles primitives.
{|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.
==DirectX 10 : les ''geometry shaders''==
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>
mxkdlhiz7xik401a91iaow6jc3nzha1
763430
763429
2026-04-10T20:13:40Z
Mewtow
31375
763430
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons étudié 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.
==Un exemple d'utilisation des nouveaux ''shaders'' : 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.
==DirectX 10 : les ''geometry shaders''==
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>
790e1459kiyx4gifnpojpfvoqbz8d7e