Un raytracer GPU

Cet article a pour objectif de présenter et décrypter une vidéo de Iñigo Quilez, demomaker et employé chez Pixar. Dans cette vidéo, il explique comment créer un raytracer en utilisant le fragment (ou pixel) shader. Je me suis permis néanmoins d’apporter quelques modifications au code original afin de rendre l’explication plus simple.

Le code est exécutable avec le site ShaderToy ou via mon logiciel FragEditor. Ces deux outils on exactement le même fonctionnement :

- Ils dessinent une surface rectangulaire qui occupe l’ensemble de la vue

- Le vertex shader est réduit au plus simple appareil (définir les positions)

- Le fragment shader est alors exécuté pour chaque pixel de l’écran.

C’est globalement le processus appliqué lors du post-processing par exemple (ajout d’effet de flou sur l’ensemble de l’image…). Ce type de fonctionnement est assez peu utilisé en situation réelle. Les sites comme ShaderToy on surtout pour objectif de présenter des performances algorithmiques, que du contenu utilisable dans un jeu par exemple.

Contrairement à la logique à laquelle on est habitué en manipulant des images côté CPU, ou on parcourt l’image et on définit séquentiellement pour chaque pixel de coordonnée x,y une couleur (ce qu’on appel la rasterization), ici le fonctionnement est tout autre, puisque l’algorithme définit dans le pixel shader est exécuté indépendamment pour chaque pixel de l’écran. Pour une vue de 1920×1080 par exemple, le code sera exécuté + de 2 millions de fois. Si on désire maintenir 60 fps, il faudra 180M d’exécutions par seconde. C’est là qu’on voit la force de parallélisation des cartes graphiques modernes ;)

Les variables du pixel shader

En entrée, on a globalement besoin de trois choses seulement :

- La résolution de la vue donnée par iResolution (unResolution dans la vidéo de Iñigo Quilez.

- La coordonnée du pixel à laquelle est exécuté le shader, donnée par gl_FragCoord. On a gl_FragColor.x est donc compris dans l’intervalle [0; iResolution.x] et gl_FragColor.y dans [0; iResolution.y]

- Le temps, iGlobalTime (ou time) pour animer éventuellement la scène. Il correspond au nombre de millisecondes écoulées depuis le démarrage de l’application.

En sortie, on a la couleur du pixel, qu’on affecte à gl_FragColor ou simplement fragColor.

Fonctionnement d’un raytracer :

Le raytracing, ou lancer de rayon consiste (cf. Wikipedia) pour chaque pixel de l’image à générer, à lancer un rayon depuis le point de vue (la caméra) dans la scène 3D. Le premier point d’impact du rayon sur un objet définit l’objet concerné par le pixel correspondant.

Des rayons sont ensuite lancés depuis le point d’impact en direction de chaque source de lumière pour déterminer sa luminosité (est-il éclairé ou à l’ombre d’autres objets ?). Cette luminosité combinée avec les propriétés de la surface de l’objet (sa couleur, sa rugosité, etc.) ainsi que d’autres informations éventuelles (angles entre la normale à l’objet et les sources de lumières, réflexions, transparence, etc.) déterminent la couleur finale du pixel.

Ainsi, on va devoir lancer un rayon provenant d’une caméra fictive situé derrière l’écran, et cela pour chaque pixel.

1) Définition de l’espace caméra

On commence par définir les coordonnées du pixel courant entre -1 et 1, que l’on corrige par l’aspect ratio. On peut alors calculer la direction du rayon qui passe par ce pixel. Ici on suppose que la caméra est placée derrière nous et regarde dans la direction + Z

cam

2) Afficher une sphère

Pour commencer, afin de simplifier, on décide d’afficher une couleur noire par défaut, à moins que notre rayon intersecte un objet. On définit ainsi la fonction intersection :

inter0

Et on calcule l’intersection avec une sphère centrée en 0,0,0. On est dans la sphère pour tout vecteur validant |xyz| = r, soit (xyz)² = r²

Ici xyz = ro + d *rd. On cherche à déterminer t, la distance d’intersection entrer le rayon est la sphère.

On pose : (ro + t * rd)² = r²

Soit ro² + t²rd² + 2.ro.rd.t – r² = 0

rd est un vecteur direction de norme 1, donc rd² = 1, ainsi

d² + 2ro.rd.t + ro² -r² = 0

Qui est un polynôme du second degré de la forme ax² + bx + c = 0

On calcule Δ = b² – 4.ac, avec

a = 1, b = 2.ro.rd et c = ro² – r²

Si Δ n’a pas de solution, il n’y a pas d’intersection, on retourne alors -1.0

Sinon on calcule la solution, t = (-b +√Δ) / 2.a

sphere

On obtient alors :

result0

3) Afficher un plan

L’équation d’un plan (xOz) est y = 0

Soit ro.y + d *rd.y = 0

Ainsi, d = – ro.y / rd.y

plan

4) Ajouter de la lumière

Qui dit lumière, dit calcul de normale. En effet la lumière diffuse dépend de l’angle entre un rayon lumineux et la normale de la surface.

Dans le cas d’une sphère, la normale sera donnée simplement le vecteur direction qui va du centre de la sphère au point d’intersection. Dans le cas d’un plan (xOz), c’est le vecteur [0.0; 1.0; 0.0].

Le facteur de diffusion est alors donné par le produit scalaire entre le vecteur lumière et le vecteur normale. On peut également ajouter une lumière ambiante.

light

Voici un exemple de ce qu’on peut obtenir en jouant sur l’orientation de la lumière et les matériaux.

result

 

C’est tout pour aujourd’hui. Les techniques utilisées sont globalement relativement simples mais pas évidentes à mettre en place au premier abord, quand on est habitué à la rasterization classique. Dans son live coding, Iñigo ajoute un peltit trick pour simuler de l’ombre sous sa sphère. Perso je me suis plutôt amusé à ajouter de l’atténuation sur la surface plane.

Le fichier GLSL de cet exemple : myraytracer.glsl

 

 

 

 

 

 

 

 

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Vous pouvez utiliser ces balises et attributs HTML : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>