mirror of
https://github.com/patriciogonzalezvivo/thebookofshaders
synced 2024-11-08 01:10:27 +00:00
294 lines
17 KiB
Markdown
294 lines
17 KiB
Markdown
![Alice Hubbard, Providence, United States, ca. 1892. Photo: Zindman/Freemont.](froebel.jpg)
|
||
|
||
## Formes
|
||
|
||
Enfin! Tous ce que nous avons appris nous a mené jusqu'à ce moment!
|
||
Nous avons vu la plupart des bases, des types et des fonctions,
|
||
nous avons répété nos fonctions de formes à une dimension, il est temps de faire marcher tout ça ensemble et vous êtes paré!
|
||
Dans ce chapitre, vous apprendrez à dessiner des formes, de façon procédurale et en parallèle sur un GPU.
|
||
|
||
### Rectangle
|
||
|
||
Imaginons que nous avons un papier millimétré, comme à l'école, et nos devoirs consistent à dessiner un carré.
|
||
la taille de la feuille est 10x10 et le carré doit mesurer 8x8, comment faire?
|
||
|
||
![](grid_paper.jpg)
|
||
|
||
A priori nous allons colorier tout sauf: la première et la dernière rangée et la première et la dernière colonne, c'est bien ça?
|
||
|
||
En quoi est-ce lié aux shaders?
|
||
Chaque petit carré du papier millimétré est un thread (un pixel, ou fragment).
|
||
chaque petit carré connaît sa position, comme des coordonnées sur un échiquier.
|
||
Dans les chapitres précédents, nous avons appris à nous servir des valeurs normalisées, par exemple, nous avons mappé ces positions *x* et *y* vers les canaux *rouge* et *vert*.
|
||
Comme nos valeurs étaient normalisées entre 0.0 and 1.0, on pouvait les utiliser comme des couleurs, ou dans les fonctions de formes, ou dans les interpolations.
|
||
Mais à présent, comment utiliser ces valeurs *x* et *y* normalisées pour dessiner un carré au centre du canvas?
|
||
|
||
Commençons par utiliser un pseudocode se servant d'une condition ```if/else``` sur toute la taille du canvas.
|
||
Le procédé est très proche de la démarche que nous avons eu avec le papier millimétré.
|
||
|
||
```glsl
|
||
if ( (X SUPERIEUR A 1) AND (Y SUPERIEUR A 1) )
|
||
dessine en blanc
|
||
else
|
||
dessine en noir
|
||
```
|
||
|
||
Nous avons une meilleure idée du code qu'il va falloir produire, nous allons remplacer les ```if``` par des [```step()```](../glossary/?search=step)
|
||
et au lieu d'utiliser 10x10, nous utiliserons les valeurs *x* et *y* normalisées entre 0.0 et 1.0:
|
||
|
||
```glsl
|
||
uniform vec2 u_resolution;
|
||
|
||
void main(){
|
||
vec2 st = gl_FragCoord.xy/u_resolution.xy;
|
||
vec3 color = vec3(0.0);
|
||
|
||
// chaque appel à step() renverra soit: 1.0 (blanc), soit 0.0 (noir).
|
||
float gauche = step(0.1,st.x); // équivalent à: si( X supérieur à 0.1 )
|
||
float bas = step(0.1,st.y); // équivalent à: si( Y supérieur à 0.1 )
|
||
|
||
// multiplier gauche par bas revient à faire un AND logique.
|
||
color = vec3( gauche * bas );
|
||
|
||
gl_FragColor = vec4(color,1.0);
|
||
}
|
||
```
|
||
|
||
La fonction [```step()```](../glossary/?search=step), va dessiner tous les pixels dont la valeur des *x* est inférieure à 0.1 en noir (```vec3(0.0)```) et tous les autres en blanc (```vec3(1.0)```).
|
||
Le fait de multiplier ```gauche``` par ```bas``` est équivalent à l'opérateur logique ```AND``` ; si les deux opérantions (X<.1 et Y<.1) renvoient 1.0, le résultat sera 1.0, sinon, ce sera 0.0.
|
||
Cela nous permet de dessiner 2 lignes noires, une en bas et une à gauche du canvas.
|
||
|
||
[NDT]En règle générale et bien que ce ne soit pas interdit, il est déconseillé d'utiliser les ```if``` dans un shader.
|
||
ça peut paraître contre-intuitif mais la raison est simple, si on fait un ```if``` (ce qu'on appelle un *conditional branching*),
|
||
le programme va devoir évaluer les 2 branches de toutes façons et cette évaluation va ralentir (voire anéantir) le bénifice d'utiliser le GPU.
|
||
Une stratégie pour parer à ce problème est de structurer le code de manière à éliminer les conditions, donc les branches.
|
||
En l'occurrence, se servir du résultat (0 ou 1) de ```step()``` et le multiplier par une autre variable (la couleur par exemple).
|
||
Si le ```step()``` renvoie 1, la couleur se multipliera par 1 et restera la même, si le ```step()``` renvoie 0, la couleur se multipliera par 0 donc elle passe au noir.
|
||
C'est une technique que voue retrouverez souvent dans les shaders.[/NDT]
|
||
|
||
![](rect-01.jpg)
|
||
|
||
Dans l'exemple ci-dessus, nous répétons la même opération sur chaque axe gauche et bas.
|
||
Nous pouvons économiser quelques lignes de code en passant les deux valeurs *x* et *y* au [```step()```](../glossary/?search=step) simultanément au lieu de faire deux appels à ```step()``` séparés, ce qui resemble à ça:
|
||
|
||
```glsl
|
||
vec2 limites = step(vec2(0.1),st);
|
||
float pct = limites.x * limites.y;
|
||
```
|
||
|
||
Nous avons à présent deux limites de notre rectangle: gauche et bas. Occupons nous des deux autres: haut et droite, regardez le code suivant:
|
||
|
||
<div class="codeAndCanvas" data="rect-making.frag"></div>
|
||
|
||
Décommentez les *lignes 21-22* et notez comment nous inversons les coordonnées de ```st``` (*1.0 - st*) et réutilisons le même appel à [```step()```](../glossary/?search=step).
|
||
Le ```vec2 tr``` contient à présent les réponses du test pour le coin haut droit. C'est l'équivalent numérique de: "retourner la page et appliquer la même procédure".
|
||
|
||
![](rect-02.jpg)
|
||
Notez qu'aux *lignes 18 et 22*, tous les côtés sont multipliés entre eux, ce qui revient à écrire:
|
||
|
||
```glsl
|
||
vec2 bl = step(vec2(0.1),st); // bas-gauche
|
||
vec2 tr = step(vec2(0.1),1.0-st); // haut-droit
|
||
color = vec3(bl.x * bl.y * tr.x * tr.y);
|
||
```
|
||
|
||
Intéressant n'est ce pas?
|
||
Le principe de cette technique est de se servir de [```step()```](../glossary/?search=step) et des multiplications comme d'opérateurs logiques et de retourner les coordonnées pour traiter les deux côtés de l'image.
|
||
|
||
Avant d'aller plus loin, essayez les choses suivantes:
|
||
|
||
* Changer la taille et les proportions du rectangle.
|
||
|
||
* Utilisez [```smoothstep()```](../glossary/?search=smoothstep) au lieu de [```step()```](../glossary/?search=step). Notez comme les arêtes passent d'un rendu net à flou.
|
||
|
||
* Réimplémentez les conditions en utilisant [```floor()```](../glossary/?search=floor).
|
||
|
||
* Conservez votre version préférée et faites-en une version réutilisable, si possible une fonction flexible et efficace (pas de *if* par exemple).
|
||
|
||
* Créez un fonction qui dessine uniquement l'extérieur du rectangle.
|
||
|
||
* Comment placer et déplacer plusieurs rectangles sur le canvas? si vous trouvez la réponse, tentez de créer une composition à la [Piet Mondrian](http://en.wikipedia.org/wiki/Piet_Mondrian).
|
||
|
||
![Piet Mondria - Tableau (1921)](mondrian.jpg)
|
||
|
||
### Cercles
|
||
|
||
Dessiner des carrés sur du papier millimétré est assez simple mais les cercles demandent un changement d'approche, notamment lorsqu'on doit traiter une masse de *pixels*.
|
||
Une solution est de *re-mapper* le système de coordonnées de manière à pouvoir utiliser la fonction [```step()```](../glossary/?search=step) pour dessiner un cercle.
|
||
|
||
Comment?
|
||
Retournons à notre cours de math et à notre papier millimétré, on utilise un compas, on lui donne le bon rayon, on pose la pointe au centre du cercle et on trace le contour du cercle.
|
||
|
||
![](compass.jpg)
|
||
|
||
Traduire cette démarche dans un shader où chaque carré du papier est un pixel revient à *demander* à chaque pixel (ou thread ou fragment), s'il est à l'intérieur ou à l'extérieur du cercle.
|
||
Pour ce faire, nous allons donc calculer la distance de chaque fragment au centre de notre cercle.
|
||
|
||
![](circle.jpg)
|
||
|
||
Il existe plusieurs façons de calculer une distance. La plus simple est d'utiliser la fonction [```distance()```](../glossary/?search=distance), qui - en interne - calcule la longueur de la différence entre les deux points passés en arguments (dans notre cas, les coordonnées du pixel et la position du centre).
|
||
La fonction ```length()``` est simplement un raccourci pour [l'équation de l'hypoténuse](http://en.wikipedia.org/wiki/Hypotenuse) qui utilise la racine carrée [```sqrt()```](../glossary/?search=sqrt) de la somme des différences en *x* et *y*, au carré.
|
||
|
||
![](hypotenuse.png)
|
||
|
||
On peut donc utiliser indifféremment [```distance()```](../glossary/?search=distance), [```length()```](../glossary/?search=length) ou [```sqrt()```](../glossary/?search=sqrt) pour calculer la distance du pixel au centre du canvas.
|
||
Le code suivant contient ces trois fonctions et démontre sans surprise que les trois renvoient le même résultat.
|
||
|
||
* Commentez et décommentez les blocs pour utiliser les différentes manières de calculer la distance.
|
||
|
||
<div class="codeAndCanvas" data="circle-making.frag"></div>
|
||
|
||
Dans l'exemple ci-dessus, nous mappons la distance au centre du canvas sur la Valeur (la luminosité) du pixel de sortie.
|
||
Plus on est proche du centre, moins la distance est grande donc plus le pixel est sombre.
|
||
Notez que les valeurs ne montent pas énorménent (jusqu'au blanc par exemple) parce que, la plus grande distance entre un pixel et le centre ( ```vec2(0.5, 0.5)``` ) dépasse péniblement 0.5.
|
||
|
||
|
||
Prenez un moment pour observer et demandez vous:
|
||
|
||
* Que peut-on déduire de ça?
|
||
|
||
* Comment peut-on s'en servir pour dessiner un cercle?
|
||
|
||
* Modifiez le code suivant pour faire tenir le dégradé complet du cercle à l'intérieur du canvas.
|
||
|
||
[NDT]en fait, la plus grande distance entre un pixel et le centre est: ```sqrt( 2.0 ) * .5``` soit environ *0.7071* [/NDT]
|
||
|
||
### Champ de distance (Distance field)
|
||
|
||
Nous pouvons penser l'exemple ci-dessus comme étant une carte d'élévations ou les parties sombres seraient plus élevées.
|
||
Le dégradé du cercle ressemble à ce que serait un cone.
|
||
Imaginez vous au sommet de ce cone, la distance horizontale vers les bords du cone serait 0.5, elle est constante dans toutes les directions.
|
||
En choisissant à quelle hauteur on peut *couper* le cone, on obtiendra un disque plus ou moins grand.
|
||
|
||
![](distance-field.jpg)
|
||
|
||
En somme, on utilise une ré-interprétation de l'espace, en se basant sur la distance au centre plutôt que sur les coordonnées des pixels, pour créer des formes.
|
||
Cette technique s'appelle un "champ de distances" (Distance Field) et s'applique dans de nombreux contextes allant du dessin de contours à la 3D.
|
||
|
||
Essayez les choses suivantes:
|
||
|
||
* Utilisez [```step()```](../glossary/?search=step) pour passer toutes les valeurs supérieures à 0.5 en blanc et toutes les autres en noir.
|
||
|
||
* Inverser les couleurs d'avant plan et d'arrière plan.
|
||
|
||
* Utilisez [```smoothstep()```](../glossary/?search=smoothstep) et changez les paramètres pour ajouter un contour flou au cercle.
|
||
|
||
* Quand vous obtenez un résultat satisfaisant, créez une fonction que vous pourrez réutiliser.
|
||
|
||
* Ajoutez de la couleur.
|
||
|
||
* Pouvez-vous animer le cercle pour qu'il grossisse et rapetisse? pour qu'il simule un battement de coeur? (les animations du chapitre précédent vous aideront).
|
||
|
||
* Comment déplacer ce cercle? pouvez vous le déplacer et placer plusieurs cercles sur le même canvas?
|
||
|
||
* Que se passe-t'il quand on combine plusieurs champs de distances en utilisant différentes fonctions et opérations?
|
||
|
||
```glsl
|
||
pct = distance(st,vec2(0.4)) + distance(st,vec2(0.6));
|
||
pct = distance(st,vec2(0.4)) * distance(st,vec2(0.6));
|
||
pct = min(distance(st,vec2(0.4)),distance(st,vec2(0.6)));
|
||
pct = max(distance(st,vec2(0.4)),distance(st,vec2(0.6)));
|
||
pct = pow(distance(st,vec2(0.4)),distance(st,vec2(0.6)));
|
||
```
|
||
|
||
* Faites trois compositions avec ces techniques, si elles bougent, c'est encore mieux!
|
||
|
||
#### Pour la boîte à outils
|
||
|
||
La fonction [```sqrt()```](../glossary/?search=sqrt) - comme toutes les fonctions qui en dépendent - est assez gourmande en ressources.
|
||
Voici autre une technique permettant de créer un champ de distances basée sur la fonction [```dot()```](../glossary/?search=dot).
|
||
|
||
<div class="codeAndCanvas" data="circle.frag"></div>
|
||
|
||
[NDT]```dot()``` renvoie le produit scalaire de deux vecteurs: ```dot(a,b)=a.x * b.x + a.y * b.y;```[/NDT]
|
||
|
||
|
||
### Propriétés utiles des champs de Distances
|
||
|
||
![Zen garden](zen-garden.jpg)
|
||
|
||
Les champs de distance permettent de dessiner à peu près n'importe quoi.
|
||
Evidemment, plus la forme est complexe, plus l'équation sera complexe mais une fois qu'on a la formule d'une forme en particulier, il devient assez simple de la combiner avec d'autres et/ou de lui appliquer des effets comme des bords lissés, ou plusieurs contours.
|
||
C'est pourquoi les champs de distances sont utilisés pour le rendu des polices de caractères: [Mapbox GL Labels](https://www.mapbox.com/blog/text-signed-distance-fields/), [Matt DesLauriers](https://twitter.com/mattdesl), [Material Design Fonts](http://mattdesl.svbtle.com/material-design-on-the-gpu) et [comme expliqué au chapitre 7 de "iPhone 3D Programming", O’Reilly](http://chimera.labs.oreilly.com/books/1234000001814/ch07.html#ch07_id36000921).
|
||
|
||
Prenez le code suivant:
|
||
|
||
<div class="codeAndCanvas" data="rect-df.frag"></div>
|
||
|
||
On commence par déplacer le système de coordonnées au centre et à le diviser par deux pour obtenir des valeurs comprises entre -1 et 1.
|
||
A la *ligne 24*, nous visualisons le champ de distances grâce à la fonction [```fract()```](../glossary/?search=fract) ce qui nous permet de mieux voir le motif qu'il crée.
|
||
Le motif du champ de distances se répète en cercles concentriques, à la manière d'un jardin zen.
|
||
|
||
Regardons la formule du champ de distances *ligne 19*. Nous calculons la distance entre chaque position et la coordonnée ```( .3,.3 )``` ( entre ```st``` et le vecteur ```vec2(.3)```) pour chaque quadrant, c'est à ça que sert l'appel à [```abs()```](../glossary/?search=abs).
|
||
|
||
Si vous décommentez la *ligne 20*, vouz noterez que nous combinons les distances à ces 4 points en utilisant la fonction [```min()```](../glossary/?search=min) contre 0, ce qui produit un nouveau motif.
|
||
|
||
[NDT]en fait il n'y a qu'un seul point, pas '4', celui en haut à droite mais il est *reflété* dans les 3 autres *quadrants* du fait qu'on a changé la taille de l'espace *ligne 16*[NDT]
|
||
|
||
Essayez à présent de décommenter la *ligne 21*, nous utilisons cette fois la fonction [```max()```](../glossary/?search=max).
|
||
Le résultat est un rectangle aux bords arrondis.
|
||
Remarquez comme les anneaux du champ de distance deviennent de plus en plus lissse à mesure qu'ils s'éloignent du centre.
|
||
|
||
Enfin, décommentez les *ligne 27 à 29* une par une pour comprendre les différentes utilisations du champ de distances.
|
||
|
||
### Formes polaires
|
||
|
||
![Robert Mangold - Untitled (2008)](mangold.jpg)
|
||
|
||
Au chapitre des couleurs, nous avons projeté (mappé) des coordonnées cartésiennes sur des coordonnées polaires en calculant un *rayon* et un *angle* entre chaque pixel et le centre grâce à la formule suivante:
|
||
|
||
```glsl
|
||
vec2 pos = vec2(0.5)-st;
|
||
float r = length(pos)*2.0;
|
||
float a = atan(pos.y,pos.x);
|
||
```
|
||
|
||
Nous avons ré-utilisé une partie de cette formule au début du chapitre pour dessiner un cercle.
|
||
Nous calculions la distance en nous servant de [```length()```](../glossary/?search=length).
|
||
Maintenant que nous en savons plus sur les champs de distances, nous pouvons dessiner de nouvelles formes grâce aux coordonnées polaires.
|
||
|
||
La technique est un peu restrictive mais très simple: elle consiste à changer la valeur du *rayon* en fonction de l'*angle* pour obtenir une variété de formes.
|
||
Comment moduler cette longueur? Avec des fonctions de formes bien sûr!
|
||
|
||
Ci-dessous, vous trouverez les mêmes fonctions dans un espace cartésien et dans un espace polaire (entre les *lignes 21 et 25* du shader).
|
||
Décommentez les fonctions une par une dans les deux démos pour comprendre les relations qui existent entre les deux systèmes de coordonnées.
|
||
|
||
<div class="simpleFunction" data="y = cos(x*3.);
|
||
//y = abs(cos(x*3.));
|
||
//y = abs(cos(x*2.5))*0.5+0.3;
|
||
//y = abs(cos(x*12.)*sin(x*3.))*.8+.1;
|
||
//y = smoothstep(-.5,1., cos(x*10.))*0.2+0.5;"></div>
|
||
|
||
<div class="codeAndCanvas" data="polar.frag"></div>
|
||
|
||
Essayez de:
|
||
|
||
* Animer les formes.
|
||
* Combiner différentes fonctions de formes pour *creuser des trous* dans les formes pour faire des fleurs, des flocons et des engrenages.
|
||
* Utilisez la fonction ```plot()``` du chapitre sur les fonctions de forme pour ne garder que le contour.
|
||
|
||
### Combinatoires
|
||
|
||
Nous venons de voir comment moduler le rayon d'un cercle en fonction d'un angle en utilisant la fonction [```atan()```](../glossary/?search=atan) pour dessiner différentes formes.
|
||
Voyons maintenant comment utiliser ```atan()``` avec un champ de distances de façon à pouvoir utiliser les effets qu'ils permettent.
|
||
|
||
L'astuce c'est d'utiliser le nombre de côtés d'un polygone régulier pour construire un champ de distances dans un espace polaire.
|
||
Pour plus d'informations, vous pouvez vous référer [au code suivant](http://thndl.com/square-shaped-shaders.html) par [Andrew Baldwin](https://twitter.com/baldand).
|
||
|
||
<div class="codeAndCanvas" data="shapes.frag"></div>
|
||
|
||
* En reprenant cet exemple, créez une fonction qui reçoit une position et un nombre de côtés et retourne la valeur du champ de distance correspondant.
|
||
|
||
* Mélangez les champs de distances en utilisant [```min()```](../glossary/?search=min) et [```max()```](../glossary/?search=max).
|
||
|
||
* Choisissez un logo géométrique et reproduisez le avec des champs de distance.
|
||
|
||
Félicitations!
|
||
Vous avez fait le plus dur!
|
||
Faites une pause et laissez décanter ces nouveaux concepts.
|
||
Dessiner des formes dans une API de dessin, c'est facile mais ici c'est une autre histoire.
|
||
Au pays des shaders, dessiner des formes géométriques est un peu tordu et s'imposer la disciple nécessaire à la compréhension de ce paradigme est épuisant.
|
||
|
||
Maintenant que vous savez comment dessiner des formes, je suis sûr que ça va vous donner des idées.
|
||
Au prochain chapitre, nous apprendrons à déplacer, à appliquer des rotations et à changer d'échelle pour créer des compositions!
|