WebGL, techniques de rendu et shaders

5 February 2012 par richar_a

Actuellement, WebGL est encore une technologie en cours de déploiement chez la plupart des personnes. Cette technologie est jeune, efficace, et commence à arriver sur tous les nouveaux navigateurs.

C’est donc une technologie sur laquelle on peut miser pour des projets innovants, afin de réaliser des projets attirants.

Préparation

Pré requis sur WebGL

Dans cet article, je ne détaillerai pas toute la base de WebGL. Beaucoup de sujet, articles, tutoriels parlent de la base de WebGL, pour afficher des objets ou des textures.

À la place, cet article porte plutôt sur les shaders, à vous de créer la base de programme qui va avec, afin d'afficher des objets, textures, et de charger des shaders basiques.

Si vous n’avez pas ces bases, je vous invite d’abord à visiter les liens dans la partie références de mon article.

Présentation des shaders

Vérifions que vous avez compris comment le pipeline d’OpenGL marche : vous stockez dans un buffer la position de vos différentes vertices (points 3D), leur normale, leur coordonnées de texture, et autres informations relative à chaque point de votre objet 3d, dans son espace local.

Lors du rendu, ces vertices sont envoyés au processeur graphique qui exécute un vertex shader, programme qui s'occupe de les transformer afin d'avoir leur position finale sur la surface de rendu, et de faire toutes sortes de calculs relatifs à chaque vertex.

Toutes les données en sortie du vertex shader sont interpolées pour obtenir les même informations mais au niveau de chaque pixel de chaque triangle.

Ces données sont passées au fragment shader, un programme qui s’exécute sur chaque pixel rendu, et qui retourne la couleur finale du pixel.

Bien sûr, il est souvent beaucoup moins coûteux de faire vos calculs (l'éclairage par exemple) dans le vertex shader que dans le fragment shader puisque le premier s’exécute pour chaque vertex plutôt que pour chaque pixel, donc beaucoup moins de fois. Néanmoins, Le fait de calculer l'éclairage sur un fragment shader permet de simuler des niveaux de détails plus élevés, et il est souvent plus intéressant de l'utiliser de cette façon. Nous verrons dans les exemples plus bas comment en tirer parti lors d'un normal mapping.

Intégration dans WebGL

Historiquement, les shaders ont d'abord été codés en assembleur, à l'époque de DirectX 8. Puis Nvidia a créé le Cg, un langage se rapprochant du C et compilant les shaders. Puis encore d'autres langages sont apparus (HLSL pour Directx, GLSL pour OpenGL, d'autres encore selon les utilisations) dérivés du Cg. Aujourd'hui, en WebGL, nous pouvons utiliser du GLSL, car WebGL/OpenGL fourni directement un compilateur.

Normalement, si vous avez suivi les bases de WebGL, vous avez forcément créé un shader simple pour afficher votre objet. Si vous avez juste voulu afficher votre objet texturé tel que décrit dans divers tutoriels vous devriez avoir quelque chose comme ça :

Vertex shader :

  attribute vec3 aVertexPosition;
  attribute vec2 aTextureCoord;
 
  uniform mat4 uMVMatrix;
  uniform mat4 uPMatrix;
 
  varying vec2 vTextureCoord;
 
  void main(void) {
    gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
    vTextureCoord = aTextureCoord;
  }

Fragment shader :

  varying vec2 vTextureCoord;
 
  uniform sampler2D uSampler;
 
  void main(void) {
    gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
  }

Donnons quelques détails sur l'utilisation des mots-clés :

  • attribute permet de définir une variable relative à une source de vertices
  • uniform définit une variable comme constante lors du traitement de la primitive (liste de triangles, de polygones, etc.)
  • varying définit les valeurs passées au pixel shader.

vec2, vec3, vec4 représentent des vecteurs de deux, trois ou quatre dimensions, et mat4 représente une matrice 4x4. sampler2D représente un texture 2D (avec ses différents niveaux de détails et informations de mapping).

Dans cet exemple, nous supposons que vous aurez défini les différentes sources, matrices et textures. Ainsi, uMVMatrix représente la matrice model-view, qui permet de passer d'une position locale en position vue par une caméra, et uPMatrix la matrice de projection, qui permet de passer à une position relative à la surface de rendu.

Les shaders par la pratique

Exemple : Le normal mapping

Le normal mapping est une technique qui permet de simuler un niveau de détail en déformant les normales de la surface au niveau de chaque pixel, afin de transformer l'éclairage en fonction de celles-ci.

Généralement, l'intensité de la lumière diffuse, par rapport à un pixel, est un produit scalaire entre la normale de la surface et le vecteur allant du pixel à la source de lumière :

Calcul diffuse

Calcul diffuse

Puis la lumière spéculaire est ajoutée par dessus la lumière diffuse : ce sont les reflets de la source de lumière qui sont représentés. Une lumière ambiante (constante) y est aussi ajoutée.

La couleur finale du pixel découle finalement de cette formule: texture * diffuse + spéculaire + ambiante.

Éclairage classique

Éclairage classique

Dans le cas du normal map, les normales sont déformées dans le pixel shader grâce à une normal map, texture qui représente les déformations dans l'espace de la surface.

Éclairage avec normal map

Éclairage avec normal map

Commençons maintenant par regarder le vertex shader :

attribute vec3 aVertexPosition;
attribute vec3 aVertexNormal;
attribute vec2 aVertexTex;
attribute vec3 aVertexBinormal;
attribute vec3 aVertexTangent;
 
uniform mat4 uMVPMatrix;
uniform mat4 uMVMatrix;
 
varying vec4 vPosition;
varying vec3 vNormal;
varying vec3 vTangent;
varying vec3 vBinormal;
varying vec2 vTex;
 
void main(void) {
    gl_Position = uMVPMatrix * vec4(aVertexPosition, 1.0);
    vPosition = uMVMatrix * vec4(aVertexPosition, 1.0);
    vNormal = normalize(uMVMatrix * vec4(aVertexNormal, 0)).xyz;
    vTangent = normalize(uMVMatrix * vec4(aVertexTangent, 0)).xyz;
    vBinormal = normalize(uMVMatrix * vec4(aVertexBinormal, 0)).xyz;
    vTex = aVertexTex;
}

Vous remarquerez que l'on passe en plus de la normale, la tangente et la binormale de la surface, qui nous permet de savoir dans quel sens est orientée la texture. On passe aussi la position du vertex au pixel shader, pour pouvoir y calculer l'éclairage.

Du coté du pixel shader, nous avons donc :

varying vec4 vPosition;
varying vec3 vNormal;
varying vec3 vTangent;
varying vec3 vBinormal;
varying vec2 vTex;
 
uniform sampler2D NormalMap;        // Normal map
uniform sampler2D diffuseTexture;   // Texture (couleur)
 
const vec3 ambiant = vec3(0.26, 0.25, 0.24); // Couleur ambiante
const vec3 diffuse = vec3(0.7, 0.7, 0.66); // Couleur diffuse
const vec3 light_dir = vec3(-0.25, 0.3, 0.3); // Direction de la lumière
const float bump_height = 0.71; // Profondeur du normal map
 
void main(void) {
    vec3 diffuse_tex = texture2D(diffuseTexture, vTex).xyz;
 
    vec3 normal_tex = texture2D(NormalMap, vTex).xyz * 2.0 - 1.0;
    normal_tex.xy *= bump_height;
    normal_tex = normalize(normal_tex);
    vec3 normal = normal_tex.x * vTangent + normal_tex.y * vBinormal + normal_tex.z * vNormal;
 
    vec3 ld = normalize(light_dir);
    float d = max(0.0, dot(normal, ld));
    float s = max(0.0, dot(reflect(-ld, normal), normalize(-vPosition.xyz)));
    s = pow(s, 30.0);
 
    gl_FragColor = vec4(ambiant + diffuse_tex * diffuse * d + s, 1);
}

Avec en particulier la variable normal qui est transformée de l'espace de la surface vers l'espace vu par la caméra. Dans d'autres exemple, vous la trouverez aussi sous forme de multiplication de matrices.

Exemple de normal map

Exemple de normal map

Références

Tutoriels WebGL
Exemple normal map en WebGL

Tags: , ,

Laisser un commentaire