Construire un monde avec les fonctions de distance en GLSL (Raymarching)

JeGX

Traducteur

Alexandre Laurent

Récemment, la technique de raymarching, une amélioration de la technique de lancer de rayon est devenue populaire. Sa force est sa simplicité, permettant de l'exécuter sur le GPU.

Commentez

- Titre : Construire un monde avec les fonctions de distance en GLSL (Raymarching)

- Auteur : JeGX

- Traducteur : Alexandre Laurent

- Parution : 25 mai 2013

- Mise à jour : 25 mai 2013

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 JeGX. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Raymarching, fonctions de distances

- I-A. GLSL Hacker

- II. Framework GLSL pour le raymarching

- II-A. Préparation

- II-B. Sol + sphère

- II-C. Sol + tore

- II-D. Sol + boîte arrondie

- II-E. Sol + union d'une boîte arrondie et d'une sphère

- II-F. Sol + boite arrondie moins une sphère

- II-G. Fondu de primitives

- II-H. Répétition de primitives

- III. Références

- IV. Remerciements

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…

Vertex shader

void main()
{
gl_TexCoord[0] = gl_MultiTexCoord0;
gl_Position = ftransform();
}

Pixel shader

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

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

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

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

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

Sol + boîte 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

Boîte arrondie fondue avec un tore

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

Sol + répétition de boites arrondies

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)));
}

Sol + répétition de tores

III - Références

- Raymarching

- Raymarching sur GPU avec les « distance fields »

- Océan de bruit par Paulo Falcao, GLSL Sandbox

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.