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).

Image non disponible

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 :

Image non disponible

Pour désactiver la synchronisation verticale, remplacez la ligne suivante du script INIT :

 
Sélectionnez
gh_renderer.set_vsync(1)

Par :

 
Sélectionnez
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 !

Image non disponible

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
Sélectionnez
void main()
{    
  gl_TexCoord[0] = gl_MultiTexCoord0;
  gl_Position = ftransform();        
}
Pixel shader
Sélectionnez
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

Image non disponible
Sol + sphère

Les nouvelles fonctions pour générer cette scène : obj_union() et obj_sphere() :

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
vec2 distance_to_obj(in vec3 p)
{
  return obj_union(obj_floor(p), obj_sphere(p));
}

II-C. Sol + tore

Image non disponible
Sol + tore

Cette fois la seule nouvelle fonction est celle pour générer le tore :

 
Sélectionnez
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() :

 
Sélectionnez
vec2 distance_to_obj(in vec3 p)
{
  return obj_union(obj_floor(p), obj_torus(p));
}

II-D. Sol + boîte arrondie

Image non disponible
Sol + boîte arrondie

La fonction pour avoir la boîte arrondie :

 
Sélectionnez
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() :

 
Sélectionnez
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

Image non disponible
Sol + union d'une boîte arrondie et d'une sphère

La fonction pour obtenir l'union des deux primitives :

 
Sélectionnez
vec2 op_union(vec2 a, vec2 b)
{
  float d = min(a.x, b.x);
  return vec2(d,1);
}

Et la fonction distance_to_obj() :

 
Sélectionnez
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

Image non disponible
Sol + boîte arrondie moins une sphère

La fonction pour soustraire les deux primitives :

 
Sélectionnez
vec2 op_sub(vec2 a, vec2 b)
{
  float d = max(a.x, -b.x);
  return vec2(d,1);
}

Et la fonction distance_to_obj() :

 
Sélectionnez
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

Image non disponible
Boîte arrondie fondue avec un tore

Le fondu est effectué par la fonction op_blend() :

 
Sélectionnez
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() :

 
Sélectionnez
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

Image non disponible
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 :

 
Sélectionnez
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() :

 
Sélectionnez
vec2 distance_to_obj(in vec3 p)
{
  return obj_union(obj_floor(p), op_rep(p, vec3(8.0, 8.0, 8.0)));
}
Image non disponible
Sol + répétition de tores

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.