I. Introduction

Cette vidéo réalisée par Iñigo Quilez, célèbre figure de la demoscene, talentueux codeur et actuellement employé chez Pixar montre en 20 minutes comment réaliser un raytracer entièrement GPU. En effet, ici, seul du code GLSL pour le pixel shader sera utilisé pour implémenter les fonctionnalités du raytracer.

Vous pouvez utiliser le GLSL Live Editor pour expérimenter vous-même le code de Iñigo Quilez

II. Vidéo


Un raytracer GPU en vingt minutes


III. Explications

III-A. Prérequis

Lorsque l'on souhaite faire un raytracer dans un pixel shader, il faut se rappeler de quelques principes simples. Le pixel shader va être exécuté sur chaque pixel de l'écran. La variable gl_FragCoord contiendra la position du pixel actuel. Il est possible de récupérer la résolution avec la variable unResolution.

III-B. Lancer de rayon

Les rayons partent de la caméra, disons (0,1,0) et ont une direction dépendante de la position du pixel. L'idée est de placer le centre de la caméra au centre de l'écran et de faire partir les rayons vers l'avant, avec un léger décalage pour chaque pixel. C'est possible de déterminer ce léger décalage si on transpose la position d'un pixel sur un espace d'une taille (-1,1) pour l'axe X et Y.

Pour ce faire, le code utilisé est :

 
Sélectionnez
// Les pixels sur un espace de 0 à 1
vec2 uv = gl_FragCoord.xy/unResolution.xy;

// La création du rayon (un rayon, c'est un point de départ 'ro' et une direction 'rd')
vec3 ro = vec3(0.0, 1.0, 0.0);
vec3 rd = normalize(vec3(-1.0+2.0*uv,-1.0));

III-C. Intersection

Une fois le rayon en place, il faut déterminer si celui-ci touche un objet. Si c'est le cas, la fonction d'intersection de l'objet retourne un nombre permettant d'identifier cet objet. Le code qui suit n'est qu'un simple test d'indice pour savoir quelle couleur appliquer suivant l'objet touché.

 
Sélectionnez
float id = intersect(ro,rd);

vec3 col = vec3(0.0); // Noir par défaut
if (id > 0.0) // Nous avons touché un objet
{
   col = vec3(1.0); // Au tout début, nous faisons que l'objet soit blanc
   // Par la suite, on peut tester l'identifiant pour assigner une couleur qui diffère pour chaque objet et plus encore
}

III-C-1. Détection d'une sphère

L'équation d'une sphère dont le centre est à l'origine du repère est :

kitxmlcodeinlinelatexdvp|xyz|=rfinkitxmlcodeinlinelatexdvp ou encore kitxmlcodeinlinelatexdvp|xyz|^2=r^2finkitxmlcodeinlinelatexdvp

Dans le cas d'un raytracer, xyz valent ro + t*rd, ce qui donne :

kitxmlcodelatexdvp|ro|^2 + t^2 + 2<ro,rd>t - r^2 = 0finkitxmlcodelatexdvp

qui est une équation quadratique que nous implémentons de la sorte :

 
Sélectionnez
float iSphere(in vec3 ro, in vec3 rd)
{
    float r = 1.0;
    float b = 2.0*dot(ro,rd);
    float c = dot(ro,ro)-r*r;
    if ( h < 0.0 ) return -1.0;
    float t = (-b - sqrt(h))/2.0;
    return t;
}

III-C-2. Détection plan

Dans le cas d'un plan s'étalant sur les axes X et Z et ayant pour position Y=0, l'équation est :

kitxmlcodelatexdvpy=0=ro.y+t*rd.yfinkitxmlcodelatexdvp

Ce que l'on peut coder comme suit :

 
Sélectionnez
float iPlane(in vec3 ro, in vec3 rd)
{
    return -ro.y/rd.y;
}

III-C-3. Intersections de plusieurs objets

Ensuite, pour effectuer l'intersection de plusieurs objets, il faut combiner les différents résultats des fonctions iPlane() et iSphere(). En effet, ces fonctions retournent la distance dans le rayon à laquelle l'intersection a lieu. Il suffit de trouver l'objet dont l'intersection est la plus petite et de retourner son identifiant.

 
Sélectionnez
float intersect(in vec3 ro, in vec3 rd)
{
    float resT=1000.0;
    float id = -1.0;
    float tsph = iSphere(ro,rd);
    float tpla = iPlane(ro,rd);
    if ( tsph > 0.0 )
    {
        id = 1.0;
        resT = tsph;
    }
    if ( tpla>0.0 && tpla < resT )
    {
        id = 2.0;
        resT = tpla;
    }
    return id;
}

III-D. Lumières

Pour les calculs des effets de lumière, il est nécessaire de connaître la normale de l'objet éclairé.

III-D-1. Calcul des normales

III-D-1-a. Sphère

La normale de la sphère est très facile à déterminer car c'est simplement la position du point d'intersection (un point sur la surface de la sphère) moins le centre. Il ne faut pas oublier de normaliser le résultat :

 
Sélectionnez
vec3 nSphere(in vec3 pos, in vec4 sph)
{
    return normalize(pos-sph);
}

III-D-1-b. Plan

La normale du plan n'a pas besoin d'être calculée, car nous avons défini le plan comme étant une surface suivant les axes X et Z. Donc la normale est (0.0,1.0,0.0).

III-D-2. Lumière diffuse

Dans cette vidéo, seule la lumière diffuse est calculée. Pour ce faire, il suffit de calculer le produit scalaire de rayon de la lumière avec la normale :

 
Sélectionnez
float dif = dot(normal, light);
float col = vec3(1.0,0.8,0.6)*dif;

III-D-3. Ombre

Le calcul de l'ombre est réalisé en utilisant une triche, car la position de la sphère est connue. En effet, la couleur du plan va légèrement être atténuée sous la sphère. Cette atténuation va être appliquée au calcul de la lumière ambiante pour le plan :

 
Sélectionnez
// sph1.w est la taille de la sphère
float amb = smoothstep(0.0, sph1.w, length(pos.xz-sph1.sz));

IV. Commenter

Vous pouvez commenter et donner vos avis dans la discussion associée sur le forum.