Les shaders changent le rôle de la carte graphique

Sujet complètement assumé : les shaders. Pas parce qu’ils vont améliorer mes formulaires, mais parce qu’ils changent carrément le travail de la carte graphique. Ça me fait tripper.

Dans un jeu, le CPU construit le monde : positions des objets, animation, caméra, lumières et commandes de dessin. Il envoie des sommets et des textures à la carte graphique par Direct3D ou OpenGL. La carte fait ensuite passer ces données dans un pipeline. Une partie de ce pipeline était autrefois presque fixe; les vertex shaders et pixel shaders permettent maintenant d’en programmer deux étapes.

sommets → vertex shader → assemblage des triangles → rasterisation
        → pixel shader + textures → profondeur/mélange → image

Le vertex shader s’exécute pour chaque sommet. Il reçoit une position, une normale, des coordonnées de texture et quelques constantes comme les matrices du monde et de la caméra. Il transforme la position 3D vers l’écran et peut déplacer le sommet pour une vague ou une animation. Une version simplifiée ressemble à ceci :

positionEcran = MatriceProjection × MatriceVue × MatriceMonde × positionObjet
normaleMonde  = MatriceNormales × normaleObjet

Après l’assemblage, la rasterisation détermine quels pixels sont couverts par chaque triangle et interpole les valeurs entre ses sommets. Le pixel shader s’exécute sur ces fragments. Il peut lire une texture, calculer l’éclairage et produire une couleur :

lumiere = max(0, produitScalaire(normale, directionLumiere))
couleur = texture(coordUV) × couleurLumiere × lumiere

Ce petit programme peut donner une matière métallique, une surface bosselée ou un reflet. Les unités du GPU exécutent le même genre d’instructions sur beaucoup de sommets ou pixels, ce qui convient au parallélisme graphique. La carte ne devient toutefois pas un processeur général sans limite. Les premiers modèles de shaders ont peu d’instructions, de registres et de branchements; les capacités diffèrent selon la carte et la version de DirectX.

À 1 024 × 768 et 60 images par seconde, l’écran contient déjà plus de 47 millions de pixels par seconde, avant de compter qu’un pixel peut être dessiné plusieurs fois par des objets superposés. Ajouter quatre lectures de texture et plusieurs calculs à chaque passage coûte vite très cher. Les textures peuvent aussi dépasser la mémoire vidéo, forçant des transferts sur le bus AGP.

Tout ce que l’image ne calcule pas

Le moteur gagne souvent plus en retirant du travail qu’en raccourcissant une instruction :

  1. éliminer les objets hors du volume de la caméra (frustum culling);
  2. éviter les surfaces orientées à l’opposé (back-face culling);
  3. choisir un modèle moins détaillé pour un objet lointain;
  4. regrouper les objets qui partagent textures et états;
  5. trier pour réduire les changements coûteux du pipeline;
  6. mesurer les passes où beaucoup de pixels sont finalement cachés.

Un objet derrière un mur est plus difficile à détecter qu’un objet hors caméra, mais des structures spatiales et des requêtes d’occlusion peuvent aider. Le résultat doit rester correct quand l’objet devient visible; un culling trop agressif produit des apparitions soudaines.

La compatibilité complique le beau shader. Le moteur devrait demander à la carte ses capacités, compiler ou charger une variante compatible et prévoir un chemin plus simple. Si une texture manque, afficher une matière de secours vaut mieux que rendre l’objet invisible. Une erreur de shader doit être journalisée avec la carte, le pilote et le profil demandé, parce que « écran noir » ne donne pas beaucoup d’indices.

Je testerais mon effet avec une scène fixe, un compteur du temps CPU et GPU, puis une version sans le shader. Je change une variable à la fois : résolution, nombre de lumières, taille des textures et nombre d’objets. Si le coût quadruple avec le nombre de pixels, je sais où regarder.

Les shaders donnent aux artistes et développeurs un contrôle incroyable, mais ce contrôle entre dans un budget de quelques millisecondes et une matrice de cartes différentes. La pratique qui me paraît la plus solide est d’écrire d’abord un chemin simple qui fonctionne partout, puis d’ajouter l’effet mesuré avec un fallback. La belle surface ne doit jamais cacher le pipeline qui doit la produire soixante fois par seconde.