I. Raymarching, fonctions de distances▲
Il y a quelque temps, j'ai trouvé cette page : modélisation avec les fonctions de distance écrite par iq, le magicien du GLSL. Cette page contient plusieurs fonctions prêtes à l'emploi pour créer des primitives en GLSL. Fonctions prêtes à l'emploi… seulement si vous avez un pixel shader prêt à les utiliser. À vrai dire, toutes ces fonctions sont conçues pour le raymarching basé sur les « distance fields ». Le raymarcher repose sur un lancer de rayon avec de grands pas suivant le rayon et une fonction de distance retournant la distance minimale jusqu'à n'importe quelle surface de la scène, depuis n'importe quel point. Vous trouverez plus de détails dans la présentation de iq : afficher le monde avec deux triangles.
Le but de cet article est de fournir un framework GLSL basique pour utiliser les fonctions de distance aussi bien que ces fonctions.
La technique de raymarching est l'un des secrets à la base de ces impressionnantes démonstrations.
I-A. GLSL Hacker▲
J'ai utilisé le logiciel GLSL Hacker pour coder et jouer avec la technique de raymarching (avec la version 0.5.0). Vous pouvez retrouver et hacker toutes les démonstrations dans le pack d'exemples de GLSL Hacker, dans le dossier GLSL_Raymarching/.
Comment exécuter les démonstrations : démarrez GLSL Hacker et chargez chaque fichier de démonstration (fichiers *.xml). C'est tout. Pour peaufiner les shaders de raymarcher, le « live coding » est un outil efficace (voir ICI et ICI).
Tous les shaders de cet article ne sont pas liés à GLSL Hacker. Vous pouvez les utiliser dans n'importe quelle application pouvant charger des shaders GLSL et bien sûr, dans les outils WebGL comme ShaderToy ou GLSL Sandbox.
Dans toutes les démonstrations GLSL Hacker, j'ai activé VSYNC (la synchronisation verticale) pour limiter le taux de rafraîchissement, car les pilotes NVIDIA n'aiment pas certains shaders effectuant du raymarching :
Pour désactiver la synchronisation verticale, remplacez la ligne suivante du script INIT :
gh_renderer.set_vsync(1)
Par :
gh_renderer.set_vsync(0)
II. Framework GLSL pour le raymarching▲
II-A. Préparation▲
Donc, de quoi avons-nous besoin pour créer des mondes avec le raymarching ? C'est très simple : un quadrilatère (deux triangles) et un programme GLSL constitué d'un vertex shader et d'un pixel shader. Ce programme GLSL shader est utilisé pendant le rendu du quadrilatère. C'est tout !
Voici le code de base pour afficher l'image ci-dessus : un sol rendu avec le raymarcher. Les shaders de raymarcher suivants ne feront que mettre à jour la fonction distance_to_obj(). C'est tout…
void
main
(
)
{
gl_TexCoord[0
] =
gl_MultiTexCoord0;
gl_Position
=
ftransform
(
);
}
uniform
vec3
cam_pos;
uniform
float
time;
uniform
vec2
resolution;
//uniform vec2 mouse;
float
PI=
3
.14159265
;
// Sol
vec2
obj_floor
(
in
vec3
p)
{
return
vec2
(
p.y+
10
.0
,0
);
}
// Union d'objets
vec2
distance_to_obj
(
in
vec3
p)
{
return
obj_floor
(
p);
}
// Couleur du sol (damier)
vec3
floor_color
(
in
vec3
p)
{
if
(
fract
(
p.x*
0
.2
)>
0
.2
)
{
if
(
fract
(
p.z*
0
.2
)>
0
.2
)
return
vec3
(
0
,0
.1
,0
.2
);
else
return
vec3
(
1
,1
,1
);
}
else
{
if
(
fract
(
p.z*
.2
)>
.2
)
return
vec3
(
1
,1
,1
);
else
return
vec3
(
0
.3
,0
,0
);
}
}
// Couleur de la primitive
vec3
prim_c
(
in
vec3
p)
{
return
vec3
(
0
.6
,0
.6
,0
.8
);
}
void
main
(
void
)
{
vec2
q =
gl_TexCoord[0
].xy;
vec2
vPos =
-
1
.0
+
2
.0
*
q;
// Inclinaison de la caméra.
vec3
vuv=
vec3
(
0
,1
,0
);
// Direction de la caméra.
vec3
vrp=
vec3
(
0
,0
,0
);
//float mx=mouse.x*PI*2.0;
//float my=mouse.y*PI/2.01;
//vec3 prp=vec3(cos(my)*cos(mx),sin(my),cos(my)*sin(mx))*6.0;
vec3
prp =
cam_pos;
// Configuration de la caméra.
vec3
vpn=
normalize
(
vrp-
prp);
vec3
u=
normalize
(
cross
(
vuv,vpn));
vec3
v=
cross
(
vpn,u);
vec3
vcv=(
prp+
vpn);
//vec3 scrCoord=vcv+vPos.x*u*resolution.x/resolution.y+vPos.y*v;
vec3
scrCoord=
vcv+
vPos.x*
u*
0
.8
+
vPos.y*
v*
0
.8
;
vec3
scp=
normalize
(
scrCoord-
prp);
// Raymarching.
const
vec3
e=
vec3
(
0
.02
,0
,0
);
const
float
maxd=
100
.0
; // Profondeur maximale
vec2
d=
vec2
(
0
.02
,0
.0
);
vec3
c,p,N;
float
f=
1
.0
;
for
(
int
i=
0
;i<
256
;i++
)
{
if
((
abs
(
d.x) <
.001
) ||
(
f >
maxd))
break
;
f+=
d.x;
p=
prp+
scp*
f;
d =
distance_to_obj
(
p);
}
if
(
f <
maxd)
{
// y est utilisé pour gérer les matériaux
if
(
d.y==
0
)
c=
floor_color
(
p);
else
c=
prim_c
(
p);
vec3
n =
vec3
(
d.x-
distance_to_obj
(
p-
e.xyy).x,
d.x-
distance_to_obj
(
p-
e.yxy).x,
d.x-
distance_to_obj
(
p-
e.yyx).x));
N =
normalize
(
n);
float
b=
dot
(
N,normalize
(
prp-
p));
// Simple éclairage Phong, LightPosition = CameraPosition
gl_FragColor=
vec4
((
b*
c+
pow
(
b,16
.0
))*(
1
.0
-
f*
.01
),1
.0
);
}
else
gl_FragColor=
vec4
(
0
,0
,0
,1
); // Couleur de fond
}
II-B. Sol + sphère▲
Les nouvelles fonctions pour générer cette scène : obj_union() et obj_sphere() :
vec2
obj_union
(
in
vec2
obj0, in
vec2
obj1)
{
if
(
obj0.x <
obj1.x)
return
obj0;
else
return
obj1;
}
La fonction obj_union() sera utilisée avec les autres primitives pour combiner le sol et la primitive. Cette fonction peut être comparée aux tests de profondeur.
Voyons la fonction pour générer une sphère. Cette fonction retourne la distance entre un point p et une sphère de rayon 1.9. Un vec2 est utilisé pour retourner le résultat, contenant la distance dans x et l'index du matériel dans y. Si matériel == 0, c'est le sol. Si matériel == 1 c'est une primitive.
vec2
obj_sphere
(
in
vec3
p)
{
float
d =
length
(
p)-
1
.9
;
return
vec2
(
d,1
);
}
Maintenant, nous utilisons les deux fonctions dans notre framework GLSL : nous devons simplement modifier la fonction distance_to_obj() comme suit :
vec2
distance_to_obj
(
in
vec3
p)
{
return
obj_union
(
obj_floor
(
p), obj_sphere
(
p));
}
II-C. Sol + tore▲
Cette fois la seule nouvelle fonction est celle pour générer le tore :
vec2
obj_torus
(
in
vec3
p)
{
vec2
r =
vec2
(
2
.1
,0
.5
);
vec2
q =
vec2
(
length
(
p.xz)-
r.x,p.y);
float
d =
length
(
q)-
r.y;
return
vec2
(
d,1
);
}
Le tore possède deux paramètres : le rayon majeur (2.1) et le rayon mineur (0.5).
Tout comme pour la sphère, nous mettons à jour la fonction distance_to_obj() :
vec2
distance_to_obj
(
in
vec3
p)
{
return
obj_union
(
obj_floor
(
p), obj_torus
(
p));
}
II-D. Sol + boîte arrondie▲
La fonction pour avoir la boîte arrondie :
vec2
obj_round_box
(
vec3
p)
{
float
d =
length
(
max
(
abs
(
p)-
vec3
(
2
.0
,0
.5
,2
.0
),0
.0
))-
0
.2
;
return
vec2
(
d,1
);
}
Et la fonction distance_to_obj() :
vec2
distance_to_obj
(
in
vec3
p)
{
return
obj_union
(
obj_floor
(
p), obj_round_box
(
p));
}
II-E. Sol + union d'une boîte arrondie et d'une sphère▲
La fonction pour obtenir l'union des deux primitives :
vec2
op_union
(
vec2
a, vec2
b)
{
float
d =
min
(
a.x, b.x);
return
vec2
(
d,1
);
}
Et la fonction distance_to_obj() :
vec2
distance_to_obj
(
in
vec3
p)
{
return
obj_union
(
obj_floor
(
p), op_union
(
obj_round_box
(
p), obj_sphere
(
p)));
}
II-F. Sol + boite arrondie moins une sphère▲
La fonction pour soustraire les deux primitives :
vec2
op_sub
(
vec2
a, vec2
b)
{
float
d =
max
(
a.x, -
b.x);
return
vec2
(
d,1
);
}
Et la fonction distance_to_obj() :
vec2
distance_to_obj
(
in
vec3
p)
{
return
obj_union
(
obj_floor
(
p), op_sub
(
obj_round_box
(
p), obj_sphere
(
p)));
}
II-G. Fondu de primitives▲
Le fondu est effectué par la fonction op_blend() :
vec2
obj_torus
(
in
vec3
p)
{
vec2
r =
vec2
(
2
.4
,0
.9
);
vec2
q =
vec2
(
length
(
p.xz)-
r.x,p.y);
float
d =
length
(
q)-
r.y;
return
vec2
(
d,1
);
}
vec2
obj_round_box
(
vec3
p)
{
float
d=
length
(
max
(
abs
(
p)-
vec3
(
1
.0
,0
.5
,2
.0
),0
.0
))-
0
.08
;
return
vec2
(
d,1
);
}
vec2
op_blend
(
vec3
p, vec2
a, vec2
b)
{
float
s =
smoothstep
(
length
(
p), 0
.0
, 1
.0
);
float
d =
mix
(
a.x, b.x, s);
return
vec2
(
d,1
);
}
Et la fonction distance_to_obj() :
vec2
distance_to_obj
(
in
vec3
p)
{
return
obj_union
(
obj_floor
(
p), op_blend
(
p, obj_round_box
(
p), obj_torus
(
p)));
}
II-H. Répétition de primitives▲
Peut-être l'une des fonctions les plus mystérieuses pour moi. Voici la fonction qui entraîne une répétition des boîtes arrondies :
vec2
op_rep
(
vec3
p, vec3
c)
{
vec3
q =
mod
(
p,c)-
0
.5
*
c;
return
obj_round_box
(
q);
}
Et la fonction distance_to_obj() :
vec2
distance_to_obj
(
in
vec3
p)
{
return
obj_union
(
obj_floor
(
p), op_rep
(
p, vec3
(
8
.0
, 8
.0
, 8
.0
)));
}
III. Références▲
IV. Remerciements▲
Cet article est une traduction autorisée de l'article paru sur Geeks3D.com.
Je tiens à remercier Winjerome pour sa relecture lors de la traduction de cet article, ainsi que ced pour sa relecture orthographique.