Initiation aux shaders (Partie 1)
Lecture 9 min. âą
Table des matiĂšres
- Hello world sauce shader
- Changement de repĂšre
- Dessiner des formes simples
- Champ de Distance Signé
- Conclusion

Bon je vais pas vous mentir jâai Ă peu prĂšs 3 jours dâexpĂ©rience sur le sujet, voyez plutĂŽt ça comme une sorte de cahier de route de ma dĂ©couverte du domaine.
Jâai trouvĂ© deux outils pour faciliter mon initiation:
Le premier est un peu la bible du dĂ©butant, le second permet dâexpĂ©riencer facilement ce que lâon apprend.
En route pour lâaventure đ±âđ
Hello world sauce shader
Ce qui dĂ©route en premier lorsque lâon lance shadertoy, câest lâabsence de ce que lâon connait dâhabitude.
Le main est déjà trÚs étrange:
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
}Cela ressemble vaguement Ă du C mais il y des diffĂ©rences. Le main qui sâapplle mainImage prend deux paramĂštres:
out vec4 fragColorin vec2 fragCood
Beaucoup de choses dĂ©jĂ dans ces deux paramĂštres. Dâabord les mots-clefs in/out. Ils agissent comme des sas entre le code que lâon Ă©crit et le code qui appelle le code que lâon Ă©crit.
Opérateur de liaison
indĂ©fini une variable qui peut-ĂȘtre modifiĂ© par notre code sans pouvoir impacter le code appelant, en forçant la recopie de la variableoutdĂ©fini une variable qui nâest pas initialisĂ©e par le code appelant mais qui peut ĂȘtre utilisĂ© par celui-ci Ă lâissue de lâĂ©xĂ©cution de la fonction. Câest dans les faits une rĂ©fĂ©rence.
Les vecteurs
Ensuite vient les termes vec4 et vec2, ce sont des structures de donnĂ©es semblables Ă des tableaux de float (toujours des floats), mais boostĂ© aux stĂ©roĂŻdes. Il existe en effet tout une collection de faciltĂ© dâĂ©criture.
Il est par exemple possible de déclarer un vec2 ainsi:
vec2 pos = vec2(1.0, 2.0);Et accéder à ses composantes de cette maniÚre:
float x = pos.x; // 1.0
float y = pos.y; // 2.0On peut aussi opérer des inversions de composantes
vec2 inverted_pos = pos.yx; // (2.0, 1.0) Il est aussi possible de faire de la récupération partielle de composantes
vec3 pos = vec3(1.0, 2.0, 3.0);
vec2 pos2 = pos.xz; // (2.0, 3.0)Ou mĂȘme dupliquer des composantes
vec3 pos = vec3(1.0, 2.0, 3.0);
vec3 pos2 = pos.xxx; // (1.0, 1.0, 1.0)Dernier outil que je connais, il est possible de ne pas complÚtement définir un vecteur
vec3 pos = vec3(0.0); // (0.0, 0.0, 0.0)
vec3 pos2 = vec3(4.0); // (4.0, 4.0, 4.0)Les nom de variables du main
Avant de parler de tout ça une petite remise en contexte est nécessaire.
Le mainImage nâest pas vraiment le main du programme. Un shader est une routine qui sâĂ©xĂ©cute sur chaque pixels dâune image.
Pour rappel, un pixel est la plus petite unité qui compose une image, pixel ( picture element ).
Afin de rendre une image on ne peut dĂ©cemment pas traiter un Ă un les pixels ça serait beaucoup trop long et gourmand en ressources, surtout avec les rĂ©solutions dâaujourdâhui qui sont gigantesques comme la 4k et la 8k.
LâidĂ©e qui a Ă©tĂ© choisi est de parallĂ©liser les traitements de chacun des pixels.

Cela implique que le traitement entre deux pixels est totalement indĂ©pendant car il doit sâeffectuer au mĂȘme moment.
Un pixel est composĂ© de 3 lampes, une rouge, une bleue et une verte. En fonction de la valeur de chaque composante on est capable de gĂ©nĂ©rer nâimporte quelle couleur du spectre visible.
![]()
Couleur du pixel
Le but de notre routine est de dĂ©finir lâintensitĂ© des lampes rouge, verte et bleue.
Le rĂ©sultat de notre calcul doit ĂȘtre affectĂ© dans la variable fragColor. Celle-la est un vec4, cela signifie quâelle possĂšde 4 composantes: (Rouge, Vert, Bleu, ?).
La derniĂšre est le canal alpha qui gĂšre la transparence. On ne sâen occupera pas dans cette partie.
DerniÚre chose à savoir est que chacune de ces composantes se répartissent entre 0 ( pas de lumiÚre du tout ) et 1 ( intensité maximale ).
Le code ci-dessous explique de renvoyer pour chaque pixel de lâimage un vecteur (1.0, 0.0, 0.0, 1.0) autrement dit du rouge pur. La 4Ăšme composante est Ă 1.0 pour signifier quâil nây a pas de transparence.
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
// Output to screen
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
Mais aussi du vert
fragColor = vec4(0.0, 1.0, 0.0, 1.0);
Mais aussi du bleu
fragColor = vec4(0.0, 0.0, 1.0, 1.0);
Mais aussi du jaune
fragColor = vec4(1.0, 1.0, 0.0, 1.0);
Bref vous voyez lâidĂ©e. Notre routine contrĂŽle chaque pixel de lâĂ©cran qui mis tous ensemble forme lâimage finale.
La position du pixel
Bon tout ça câest trĂšs bien mais un peu limitĂ© tous les pixels ont la mĂȘme couleur on est pas prĂšs de faire le nouveau Pixar Ă la mode. đ
Câest lĂ quâintervient notre second paramĂštre fragCoord il sâagit lĂ aussi dâun vec2, la premiĂšre composante est lâabscisse du pixel sur lâĂ©cran et la deuxiĂšme son ordonnĂ©e dans la pratique (x, y).
Essayons de visualiser cela đ§
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
// Output to screen
fragColor = vec4(fragCoord.x, 0.0, 0.0 ,1.0);
}
Rouge, encore! Mais câest normal quand on y rĂ©flĂ©chi, les coordonnĂ©es des pixels sont donnĂ©es sous leur forme cartĂ©siennes, câest Ă dire des nombres entiers positifs.
Or les composantes de couleur ne peuvent pas dépasser la valeur 1.0.
Il va donc falloir normaliser les valeurs. Pour cela on va utiliser un autre outils mis à disposition par le langage de shader qui nous fourni tout une série de variable appelée des uniforms.

Il faut les voir comme le moyens de communiquer des informations entre les diffĂ©rentes unitĂ© de traitement de pixels. Celle qui va nous intĂ©resser ici est iResolution. Elle contient la taille en pixels de lâĂ©cran et donc le nombre en X et en Y de pixels.
Ce qui va nous permettre dâeffectuer une rĂšgle de trois entre la position du pixel et la rĂ©solution.
iResolution.x -> rouge pur (1.0)
fragCoord.x -> intensité de rouge (X)
X = (fragCoord.x * 1.0) / iResolution.xEn utilisant les propriétés des vecteurs:
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord / iResolution.xy;
// Output to screen
fragColor = vec4(uv.x, 0.0, 0.0 ,1.0);
}
De mĂȘme en Y
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord / iResolution.xy;
// Output to screen
fragColor = vec4(uv.y, 0.0, 0.0 ,1.0);
}
On peut mĂȘme cartographier les deux composantes
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord / iResolution.xy;
// Output to screen
fragColor = vec4(uv.x, uv.y, 0.0 ,1.0);
}
Le noir est une absence de luminosité autrement dit (0.0, 0.0, 0.0, 1.0).
Le jaune une combinaison de rouge et de vert, le jaune pur représente (1.0, 1.0, 0.0, 1.0).
En prenant la formule Ă lâenvers on peut dĂ©terminer lâorigine du repĂšre de notre Ă©cran ainsi que le sens du repĂšre.
Pour une résolution de 800x450, il vient:

Changement de repĂšre
Bon je sais pas vous mais le repĂšre en bas Ă gauche nâa jamais Ă©tĂ© ma tasse de thĂ© ou de cafĂ© pour les amateurs.
Je prĂ©fĂšre que lâorigine de mon repĂšre soit le centre de mon Ă©cran, cela va faciliter grandement les calculs qui vont suivre. (oui il va y avoir un max de maths đ€).
Pour mettre le centre au centre on va retrancher la moitié de la résolution sur chacune des composantes.
Cette soustraction va automatiquement nous amener à obtenir des valeurs négatives. Une composante de couleur étant toujours positive, une valeur négative sera mise à 0.0.
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord - 0.5 * iResolution.xy;
// Output to screen
fragColor = vec4(uv.x, uv.y, 0.0 ,1.0);
}On se retrouve avec 4 quadrants:

Que lâon peut avec la mĂȘme technique que tout Ă lâheure visualiser de maniĂšre plus prĂ©cise sous la forme dâun continuum.
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / (iResolution.xy / 2.0);
// Output to screen
fragColor = vec4(uv.x, uv.y, 0.0 ,1.0);
}
Dessiner des formes simples
Le disque
Câest la forme la plus simple Ă afficher.
Son équation dans le plan cartésien est :
xÂČ + yÂČ < RÂČComme on a centrĂ© le repĂšre, il nây a pas besoin de se soucier dâoffset.
en prenant la racine de chaque cÎté
sqrt(xÂČ + yÂČ) < RLe langage fourni une fonction appellĂ©e length qui permet de rĂ©cupĂ©rer la norme dâun vecteur.
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / (iResolution.xy / 2.0);
// Output to screen
fragColor = vec4(uv.x, uv.y, 0.0 ,1.0);
if (length(uv) < 1.0) {
fragColor = vec4(1.0, 1.0, 1.0 ,1.0);
}
}
Bon câest presque ça, on a juste nĂ©gligĂ© le ratio de notre image. Pour rĂ©tablir le disque on peut diviser nos coordonnĂ©es par la rĂ©solution en Y sur 2.
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / (iResolution.y / 2.0);
// Output to screen
fragColor = vec4(uv.x, uv.y, 0.0 ,1.0);
if (length(uv) < 1.0) {
fragColor = vec4(1.0, 1.0, 1.0 ,1.0);
}
}
Et tada đ un disque!
Un triangle équilatéral
Je vais pas vous mentir, ça nâa pas Ă©tĂ© trĂšs facile de dĂ©terminer comment dessiner le triangle.
Le principe reste le mĂȘme: dĂ©terminer si un point appartient ou non Ă une surface.
Le problĂšme ici est que la forme est bien plus complexe quâun disque.
Il faut donc ruser pour trouver une solution
Une petite simulation avec geogebra mâa permis de me simplifier la reprĂ©sentation du problĂšme.
Si M est en dehors de lâangle AB, AC. La valeur de d est nĂ©gative.

De mĂȘme dans lâautre sens

Par contre si lâon se trouve dans lâangle, la valeur d devient positive.

Le problĂšme câest que si le point est dans lâangle mais en dehors du triangle, la valeur reste positive.

LâidĂ©e est dâappliquer lâalgorithme sur les 3 angles, pour que le point soit bel et bien Ă lâintĂ©rieur du triangle. Si chacun des produits scalaires sont positifs alors le point M appartient au triangle.
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / (iResolution.y / 2.0);
// Output to screen
fragColor = vec4(uv.x, uv.y, 0.0 ,1.0);
// Coordonnées des sommets du triangles équilatéral
float k = 1.75;
vec3 A = vec3(0.0, 0.57, 0.0) * k;
vec3 B = vec3(0.5, -0.28, 0.0) * k;
vec3 C = vec3(-0.5, -0.28, 0.0) * k;
// Calcul des coordonnées des vecteurs
vec3 AB = B-A;
vec3 AC = C-A;
vec3 BA = A-B;
vec3 BC = C-B;
vec3 CA = A-C;
vec3 CB = B-C;
// M est le point que l'on cherche à déterminer s'il est en dehors ou non du triangle
vec3 AM = vec3(uv - A.xy, 0);
vec3 BM = vec3(uv - B.xy, 0);
vec3 CM = vec3(uv - C.xy, 0);
// Calcul des déterminants
float d1 = dot(cross(AB, AM), cross(AM, AC));
float d2 = dot(cross(BA, BM), cross(BM, BC));
float d3 = dot(cross(CA, CM), cross(CM, CB));
// Dessin du cercle de rayon 1
if (length(uv) < 1.0) {
fragColor = vec4(0.0, 0.0, 1.0 ,1.0);
}
// Si tous les déterminants sont positifs, le point est dans le triangle
if (d1 >= 0.0 && d2 >= 0.0 && d3 >= 0.0) {
fragColor = vec4(1.0, 1.0, 1.0 ,1.0);
}
}Et un triangle pour la table 12 ! Un ! đšâđł

Champ de Distance Signé
Les deux algorithmes (pour le disque et pour le triangle) sont appelées des SDF : Signed Distance Field ou Champ de Distance Signé.
Un champ est une propriĂ©tĂ© qui recouvre tout un espace ( ici 2D ). Cela signifie que notre propiĂ©tĂ© va sâappliquer sur chaque pixel de lâĂ©cran.
La deuxiĂšme partie est cette idĂ©e de distance. On recheche pour tout point de lâĂ©cran si lâon est Ă lâintĂ©rieur ou lâextĂ©rieur dâune surface donnĂ©e et surtout Ă quelle distance on se trouve dâelle.
Reprenons le disque qui est un peu plus simple. Son champ de distance est donnĂ© en tout point de lâĂ©cran par la relation suivante:
Distance du point observé par rapport au centre du disque - rayon du disque
Tout ce que lâon fera par la suite sera de calculer la SDF de nos forme.
Et pourquoi âsignĂ©â, parce que si lâon est Ă lâintĂ©rieur de la du disque, la distance devient nĂ©gative.

Conclusion
Câest tout pour aujourdâhui!
La suite sera de se plonger dans le monde merveilleux de la 3D enfin de la projection de la 3D sur un écran 2D.
On y verra notamment le RayMarcher.
Bonne FĂȘtes Ă toutes et tous et merci de mâavoir lu. đ€