Apprendre à écrire des shaders graphiques, c'est apprendre à exploiter la puissance du processeur graphique, avec ses milliers de cœurs fonctionnant tous en parallèle. C'est une sorte de programmation qui nécessite un état d'esprit différent, mais libérer son potentiel en vaut la peine..
Pratiquement toutes les simulations graphiques modernes que vous voyez sont alimentées d'une manière ou d'une autre par un code écrit pour le GPU, des effets de lumière réalistes des jeux AAA à la pointe aux effets de post-traitement 2D et aux simulations de fluides.
Une scène dans Minecraft, avant et après l'application de quelques shaders.La programmation par shader se présente parfois comme une magie noire énigmatique et est souvent mal comprise. Il existe de nombreux exemples de code qui vous montrent comment créer des effets incroyables, sans toutefois vous donner d'explication. Ce guide vise à combler ce fossé. Je vais me concentrer davantage sur les bases de l'écriture et de la compréhension du code shader, afin que vous puissiez facilement modifier, combiner ou écrire le vôtre à partir de zéro.!
Ceci est un guide général, donc ce que vous apprendrez ici s'appliquera à tout ce qui peut exécuter des shaders.
Un shader est simplement un programme qui s'exécute dans le pipeline graphique et indique à l'ordinateur comment effectuer le rendu de chaque pixel. Ces programmes sont appelés shaders car ils sont souvent utilisés pour contrôler les effets d'éclairage et d'ombrage, mais rien ne l'empêche de gérer d'autres effets spéciaux..
Les shaders sont écrits dans un langage de shading spécial. Ne vous inquiétez pas, vous n'êtes pas obligé de sortir et d'apprendre une langue complètement nouvelle. nous utiliserons GLSL (OpenGL Shading Language) qui est un langage semblable à C. (Il existe de nombreux langages de shading pour différentes plates-formes, mais comme ils sont tous adaptés pour fonctionner sur le GPU, ils sont tous très similaires.)
Remarque: cet article concerne exclusivement les shaders de fragments. Si vous êtes curieux de savoir ce qui se passe ailleurs, vous pouvez en savoir plus sur le différentes étapes du pipeline graphique sur le Wiki OpenGL.
Nous allons utiliser ShaderToy pour ce tutoriel. Cela vous permet de commencer à programmer des shaders directement dans votre navigateur, sans avoir à régler quoi que ce soit! (Il utilise WebGL pour le rendu, vous aurez donc besoin d'un navigateur capable de le prendre en charge.) La création d'un compte est facultative, mais pratique pour enregistrer votre code..
Remarque: ShaderToy est en version bêta au moment de la rédaction de cet article. Certains petits détails de l'interface utilisateur / syntaxe peuvent être légèrement différents.
En cliquant sur New Shader, vous devriez voir quelque chose comme ceci:
Votre interface peut être légèrement différente si vous n'êtes pas connecté.La petite flèche noire en bas est ce que vous cliquez pour compiler votre code.
Je suis sur le point d'expliquer comment les shaders fonctionnent en une phrase. Es-tu prêt? Voici!
Le seul but d'un shader est de renvoyer quatre nombres: r
, g
, b
,et une
.
C'est tout ce que cela fait ou peut faire. La fonction que vous voyez devant vous est exécutée pour chaque pixel à l'écran. Il renvoie ces quatre valeurs de couleur, et cela devient la couleur du pixel. C'est ce qu'on appelle un Pixel Shader(parfois appelé un Fragment shader).
Dans cet esprit, essayons de transformer notre écran en rouge. Les valeurs rgba (rouge, vert, bleu et "alpha", qui définit la transparence) vont de 0
à 1
, tout ce que nous avons à faire est de revenir r, g, b, a = 1,0,0,1
. ShaderToy s'attend à ce que la couleur finale du pixel soit stockée dans fragColor
.
void mainImage (hors vec4 fragColor, dans vec2 fragCoord) fragColor = vec4 (1.0,0,0,0,0,1,0));
Toutes nos félicitations! C'est votre tout premier shader de travail!
Défi: Pouvez-vous le changer en gris uni?
vec4
est juste un type de données, donc nous aurions pu déclarer notre couleur en tant que variable, comme ceci:
void mainImage (hors vec4 fragColor, dans vec2 fragCoord) vec4 solidRed = vec4 (1.0,0,0,0,0,1.0); fragColor = solidRed;
Ce n'est pas très excitant, cependant. Nous avons le pouvoir d'exécuter du code sur des centaines de milliers de pixels en parallèle et nous les définissons tous à la même couleur.
Essayons de rendre un dégradé sur l'écran. Eh bien, nous ne pouvons pas faire grand chose sans connaître quelques choses sur le pixel que nous affectons, comme son emplacement à l'écran…
Le pixel shader transmet quelques variables que vous pouvez utiliser. Le plus utile pour nous est fragCoord
, qui contient les coordonnées x et y du pixel (et z, si vous travaillez en 3D). Essayons de transformer tous les pixels de la moitié gauche de l'écran en noir et tous ceux de la moitié droite en rouge:
void mainImage (out vec4 fragColor, dans vec2 fragCoord) vec2 xy = fragCoord.xy; // Nous obtenons nos coordonnées pour le pixel actuel vec4 solidRed = vec4 (0,0,0,0,0,1,0); // Ceci est en fait noir en ce moment si (xy.x> 300.0) // Nombre arbitraire, nous ne le faisons pas savoir à quel point notre écran est grand! solidRed.r = 1.0; // Définissez sa composante rouge sur 1.0 fragColor = solidRed;
Remarque: Pour toute vec4
, vous pouvez accéder à ses composants via obj.x
, obj.y
, obj.z
et obj.w
ouviaobj.r
, obj.g
, obj.b
, obj.a
. Ils sont équivalents; c’est un moyen pratique de les nommer pour rendre votre code plus lisible, de sorte que les autres voient obj.r
, ils comprennent que obj
représente une couleur.
Voyez-vous un problème avec le code ci-dessus? Essayez de cliquer sur le ouvrir en plein écran bouton en bas à droite de votre fenêtre d'aperçu.
La proportion de l'écran qui est rouge sera différente en fonction de la taille de l'écran. Pour nous assurer que la moitié de l'écran est rouge, nous devons connaître la taille de notre écran. La taille de l'écran est ne pas une variable intégrée telle que l'emplacement des pixels était, parce que c'est généralement à vous, le programmeur qui a construit l'application, de définir cela. Dans ce cas, ce sont les développeurs ShaderToy qui définissent la taille de l'écran..
Si quelque chose n'est pas une variable intégrée, vous pouvez envoyer ces informations de la CPU (votre programme principal) au GPU (votre shader). ShaderToy s'en occupe pour nous. Vous pouvez voir toutes les variables passées au shader dans le Entrées Shader languette. Les variables ainsi transmises du processeur au GPU sont appelées uniforme en GLSL.
Modifions notre code ci-dessus pour obtenir correctement le centre de l'écran. Nous aurons besoin d'utiliser l'entrée shader iRésolution
:
void mainImage (out vec4 fragColor, dans vec2 fragCoord) vec2 xy = fragCoord.xy; // Nous obtenons nos coordonnées pour le pixel actuel xy.x = xy.x / iResolution.x; // On divise les coordonnées par la taille de l'écran xy.y = xy.y / iResolution.y; // Maintenant, x est 0 pour le pixel le plus à gauche et 1 pour le pixel le plus à droite vec4 solidRed = vec4 (0,0,0,0,0,1,0); // Ceci est en fait noir en ce moment si (xy.x> 0.5) solidRed.r = 1.0; // Définit sa composante rouge sur 1.0 fragColor = solidRed;
Si vous essayez d'agrandir la fenêtre d'aperçu cette fois-ci, les couleurs doivent toujours parfaitement diviser l'écran en deux..
Transformer cela en dégradé devrait être assez facile. Nos valeurs de couleur vont de 0
à 1
, et nos coordonnées vont maintenant de 0
à 1
ainsi que.
void mainImage (out vec4 fragColor, dans vec2 fragCoord) vec2 xy = fragCoord.xy; // Nous obtenons nos coordonnées pour le pixel actuel xy.x = xy.x / iResolution.x; // On divise les coordonnées par la taille de l'écran xy.y = xy.y / iResolution.y; // Maintenant, x est 0 pour le pixel le plus à gauche et 1 pour le pixel le plus à droite vec4 solidRed = vec4 (0,0,0,0,0,1,0); // C'est en fait le noir en ce moment solidRed.r = xy.x; // Définit sa composante rouge sur la valeur x normalisée fragColor = solidRed;
Et le tour est joué!
Défi: Pouvez-vous transformer cela en un dégradé vertical? Qu'en est-il de la diagonale? Qu'en est-il d'un dégradé avec plus d'une couleur?
Si vous jouez assez avec cela, vous pouvez dire que le coin en haut à gauche a des coordonnées (0,1)
, ne pas (0,0)
. Ceci est important à garder à l'esprit.
Jouer avec les couleurs est amusant, mais si nous voulons faire quelque chose d’impressionnant, notre shader doit pouvoir prendre en charge les entrées d’une image et les modifier. De cette manière, nous pouvons créer un shader qui affecte l’ensemble de notre écran de jeu (comme un effet de fluide sous-marin ou une correction de couleur) ou n’affecte que certains objets de certaines manières en fonction des entrées (comme un système d’éclairage réaliste)..
Si nous programmions sur une plate-forme normale, nous aurions besoin d'envoyer notre image (ou texture) au GPU en tant que uniforme, de la même manière que vous auriez envoyé la résolution de l'écran. ShaderToy s'en occupe pour nous. Il y a quatre canaux d’entrée en bas:
Les quatre canaux d'entrée de ShaderToy.Cliquer sur iChannel0 et sélectionnez n'importe quelle texture (image) que vous aimez.
Une fois que cela est fait, vous avez maintenant une image qui est transmise à votre shader. Il y a cependant un problème: il n'y a pasDrawImage ()
une fonction. Rappelez-vous, la seule chose que le pixel shader puisse faire est changer la couleur de chaque pixel.
Donc, si nous ne pouvons que renvoyer une couleur, comment pouvons-nous dessiner notre texture à l'écran? Nous devons en quelque sorte mapper le pixel actuel sur lequel notre shader est activé, au pixel correspondant sur la texture:
En fonction de l'emplacement de (0,0) à l'écran, vous devrez peut-être retourner l'axe des y pour mapper correctement votre texture. Au moment de la rédaction de cet article, ShaderToy a été mis à jour pour avoir son origine en haut à gauche. Il n'est donc pas nécessaire de retourner quoi que ce soit..Nous pouvons le faire en utilisant la fonction texture (textureData, coordonnées)
, qui prend des données de texture et un (x, y)
paire de coordonnées en tant qu’entrées et renvoie la couleur de la texture à ces coordonnées en tant que vec4
.
Vous pouvez faire correspondre les coordonnées à l'écran comme bon vous semble. Vous pouvez dessiner la totalité de la texture sur un quart de l'écran (en sautant des pixels, ce qui la réduit effectivement) ou simplement dessiner une partie de la texture..
Pour nos besoins, nous voulons simplement voir l'image, nous allons donc faire correspondre les pixels 1: 1:
void mainImage (out vec4 fragColor, dans vec2 fragCoord) vec2 xy = fragCoord.xy / iResolution.xy; // en le condensant en une seule ligne vec4 texColor = texture (iChannel0, xy); // Obtenir le pixel en xy de iChannel0 fragColor = texColor; // Définit le pixel de l'écran avec cette couleur
Avec cela, nous avons notre première image!
Maintenant que vous extrayez correctement les données d'une texture, vous pouvez la manipuler comme bon vous semble! Vous pouvez l'étirer et le redimensionner, ou jouer avec ses couleurs.
Essayons de modifier cela avec un dégradé, semblable à ce que nous avons fait ci-dessus:
texColor.b = xy.x;
Félicitations, vous venez de faire votre premier effet de post-traitement!
Défi: Pouvez-vous écrire un shader qui transformera une image en noir et blanc?
Notez que même s’il s’agit d’une image statique, ce que vous voyez devant vous se passe en temps réel. Vous pouvez le constater vous-même en remplaçant l’image statique par une vidéo: cliquez sur le bouton iChannel0 saisissez à nouveau et sélectionnez l'une des vidéos.
Jusqu'à présent, tous nos effets ont été statiques. Nous pouvons faire beaucoup plus de choses intéressantes en utilisant les entrées que ShaderToy nous donne. iGlobalTime
est une variable en augmentation constante; nous pouvons l'utiliser comme une graine pour produire des effets périodiques. Essayons de jouer un peu avec les couleurs:
void mainImage (out vec4 fragColor, dans vec2 fragCoord) vec2 xy = fragCoord.xy / iResolution.xy; // Condensant cela en une ligne vec4 texColor = texture (iChannel0, xy); // Récupère le pixel en xy à partir d'iChannel0. TexColor.r * = abs (sin (iGlobalTime)); texColor.g * = abs (cos (iGlobalTime)); texColor.b * = abs (sin (iGlobalTime) * cos (iGlobalTime)); fragColor = texColor; // Définit le pixel de l'écran avec cette couleur
Il existe des fonctions sinus et cosinus intégrées au GLSL, ainsi que de nombreuses autres fonctions utiles, telles que la longueur d'un vecteur ou la distance entre deux vecteurs. Les couleurs ne sont pas supposées être négatives, nous nous assurons donc d'obtenir la valeur absolue en utilisant le abdos
une fonction.
Défi: Pouvez-vous créer un shader qui modifie une image en noir et blanc en couleur??
Bien que vous ayez peut-être l'habitude de parcourir votre code et d'imprimer les valeurs de tout pour voir ce qui se passe, ce n'est pas vraiment possible lorsque vous écrivez des shaders. Vous pouvez trouver des outils de débogage spécifiques à votre plate-forme, mais en général, votre meilleur choix est de définir la valeur que vous testez sur quelque chose de graphique que vous pouvez voir à la place.
Ce ne sont que les bases du travail avec les shaders, mais vous familiariser avec ces principes de base vous permettra de faire beaucoup plus. Parcourez les effets sur ShaderToy et voyez si vous pouvez en comprendre ou en reproduire certains!
Une chose que je n'ai pas mentionnée dans ce tutoriel est Vertex Shaders. Ils sont toujours écrits dans le même langage, sauf qu'ils tournent sur chaque sommet au lieu de chaque pixel et qu'ils renvoient une position ainsi qu'une couleur. Les vertex shaders sont généralement responsables de la projection d'une scène 3D sur l'écran (ce qui est intégré à la plupart des pipelines graphiques). Les pixel shaders sont responsables de la plupart des effets avancés que nous voyons. C’est pourquoi ils sont notre objectif..
Défi final: Pouvez-vous écrire un shader qui supprime l’écran vert des vidéos sur ShaderToy et ajoute une autre vidéo en arrière-plan à la première?
C'est tout pour ce guide! J'apprécierais beaucoup vos commentaires et vos questions. Si vous souhaitez en savoir plus sur quelque chose de spécifique, laissez un commentaire. Les futurs guides pourraient inclure des sujets tels que les bases des systèmes d’éclairage, ou la simulation fluide, ou la configuration de shaders pour une plateforme spécifique..