Que sont les shaders ?

Introduction

Donc, vous avez décidé d'essayer de créer des shaders. Vous avez probablement entendu qu'ils peuvent être utilisés pour créer des effets intéressants qui s’exécutent incroyablement vite. Vous avez aussi probablement entendu dire qu'ils sont terrifiants. Les deux sont vrais.

Les shaders peuvent être utilisés pour créer une large gamme d'effets (en fait, tout ce qui est affiché dans un moteur de rendu moderne est fait avec des shaders).

Écrire des shaders peut être très difficile pour des personnes peu familières avec eux. Godot essaie de rendre l'écriture de shaders un peu plus facile en proposant de nombreuses fonctionnalités intégrées et en s'occupant d'une partie du travail d'initialisation de bas niveau pour vous. Cependant, le GLSL (le langage de shader OpenGL, que Godot utilise) reste peu intuitif et restrictif, en particulier pour les utilisateur·ices habitué·e·s au GDScript.

Mais qu'est-ce que c'est ?

Les shaders sont un type de programme spécial qui s’exécutent par les processeurs graphiques (GPU - Graphics Processing Units). La plupart des ordinateurs ont un GPU, qu'il soit intégré dans leur CPU ou discret (ce qui signifie qu'il est un composant matériel distinct, comme par exemple la carte graphique). Les GPUs sont particulièrement utiles pour le rendu car ils sont optimisés pour exécuter des milliers d'instructions en parallèle.

La sortie du shader est généralement les pixels colorés de l'objet dessinés dans la fenêtre d'affichage. Mais certains shaders permettent des sorties spécialisées (ce qui est particulièrement vrai pour les API comme Vulkan). Les shaders opèrent à l'intérieur du pipeline de shaders. Le processus standard est le pipeline vertex -> fragment. Le shader vertex est utilisé pour décider où chaque vertex (point dans un modèle 3D, ou coin d'un Sprite) va et le shader de fragment décide quelle couleur les pixels individuels reçoivent.

Supposons que vous vouliez changer tous les pixels d’une texture en une couleur donnée, dans le CPU vous écririez :

for x in range(width):
  for y in range(height):
    set_color(x, y, some_color)

Dans un shader, vous n’avez accès qu’à l’intérieur de la boucle, donc ce que vous écrivez ressemble à cela :

// function called for each pixel
void fragment() {
  COLOR = some_color;
}

Vous n'avez aucun contrôle sur la manière dont cette fonction est appelée. Vous devez donc concevoir vos shaders différemment de vos programmes pour le CPU.

Une conséquence du pipeline des shader et que vous ne pouvez pas accéder au résultats des exécutions précédentes du shader, vous ne pouvez pas accéder à d'autres pixels que celui qui est en train d'être dessiné, et vous ne pouvez pas écrire en dehors de ce pixel. Cela permet au GPU d’exécuter le shader pour différents pixels en parallèle, comme ils ne dépendent pas les uns des autres. Ce manque de flexibilité est conçu pour fonctionner avec le GPU qui permet aux shaders d'être incroyablement rapide.

Ce qu'ils peuvent faire

  • positionner les sommets très rapidement
  • calculer les couleurs très rapidement
  • calculer l'éclairage très rapidement
  • beaucoup, beaucoup de maths

Ce qu'ils ne peuvent pas faire

  • dessiner en dehors des maillages
  • accéder aux pixels autres que le pixel courant (ou sommets)
  • stocker les itérations précédentes
  • mise à jour à la volée (ils peuvent l'être, mais doivent être compilés)

Structure d'un shader

Dans Godot, les shaders sont fait de trois fonctions principales : La fonction vertex(), la fonction fragment() et la fonction light().

La fonction vertex() s'exécute pour tous les sommets du mesh et définit leur position ainsi que quelques autres variables des sommets.

La fonction fragment() s'exécute pour tous les pixels couverts par le mesh. Elle utilise les variables provenant de la fonction vertex(). Les variables de la fonction vertex() sont interpolées entre les sommets pour fournir les valeurs pour la fonction fragment().

La fonction light() s'exécute pour chaque pixel et chaque lumière. Elle prends des variables de la fonction fragment() et de ses exécutions précédentes.

Pour plus d'informations sur comment les shaders fonctionnent dans Godot spécifiquement, regardez la Shaders doc.

Avertissement

La fonction light() ne sera pas exécutée si le mode de rendu vertex_lighting est activé, ou si Rendering > Quality > Shading > Force Vertex Shading est activé dans les paramètres du projet. (C'est activée par défaut sur les plateformes mobiles.)

Aperçu technique

Les GPUs sont capables de rendre des éléments graphiques beaucoup plus rapidement que les CPUs pour diverses raisons, mais surtout parce qu'ils sont capables d'exécuter massivement des calculs en parallèle. Un CPU possède généralement 4 ou 8 cœurs, tandis qu'un GPU en possède généralement des milliers. Cela signifie qu'un GPU peut effectuer des centaines de tâches à la fois. Les concepteurs de GPU ont exploité cette possibilité de manière à pouvoir effectuer de nombreux calculs très rapidement, mais uniquement lorsque plusieurs ou tous les cœurs effectuent le même calcul en même temps, avec des données différentes.

C'est là que les shaders entrent en jeu. Le GPU va appeler le shader plusieurs fois simultanément, puis traiter différents bits de données (sommets, ou pixels). Ces paquets de données sont souvent appelés wavefronts. Un shader fonctionnera de la même manière pour chaque thread du wavefront. Par exemple, si un GPU donné peut gérer 100 threads par wavefront, un wavefront fonctionnera sur un bloc de 10x10 pixels à la fois. Et il continuera à fonctionner pour tous les pixels de ce wavefront jusqu'à ce qu'ils soient achevés. Par conséquent, si vous avez un pixel plus lent que les autres (en raison d'une ramification excessive), l'ensemble du bloc sera ralenti, ce qui se traduira par des temps de rendu massivement plus lents.

Cela diffère des opérations basées sur le CPU. Sur un CPU, si vous pouvez accélérer ne serait-ce qu'un seul pixel, le temps de rendu entier diminuera. Sur un GPU, vous devez accélérer l'ensemble du wavefront pour accélérer le rendu.