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 :
// 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é.
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 = 0finkitxmlcodelatexdvpqui est une équation quadratique que nous implémentons de la sorte :
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.yfinkitxmlcodelatexdvpCe que l'on peut coder comme suit :
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.
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 :
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 :
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 :
// 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.