Normal mapping en GLSL

JeGX

Traducteur

Alexandre Laurent

Cet article vous explique comment calculer le vecteurs tangents pour vos textures et cela, directement dans le shader.

Commentez

- Titre : Normal mapping en GLSL

- Auteur : JeGX

- Traducteur : Alexandre Laurent

- Parution : 17 février 2013

- Mise à jour : 17 février 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 Geeks3D. 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. Tangent Binormal Normal

- II. Remerciements

I - Tangent Binormal Normal

C'est une belle opportunité que d'être au courant des dernières nouveautés du monde de la programmation graphique. Je viens de publier GLSL Hacker et le support des vecteurs tangents précalculés est l'une des fonctionnalités manquantes parmi des milliards dans cette première version. Les vecteurs tangents (ou Tangent Binormal Normal, soit TBN) sont utiles dans de nombreuses situations comme pour le bump/normal mapping où les TBN sont précalculés à partir des coordonnées de texture et sauvegardés comme attributs de vertex.

Lorsque les tangentes précalculées n'existent pas, j'utilise la méthode suivante pour les calculer dans le vertex shader :

vec3 t;
vec3 b;
vec3 c1 = cross(vertex_normal, vec3(0.0, 0.0, 1.0));
vec3 c2 = cross(vertex_normal, vec3(0.0, 1.0, 0.0));
if (length(c1) > length(c2))
t = c1;
else
t = c2;
t = normalize(t);
b = normalize(cross(vertex_normal, t));

Cette méthode est rapide mais approximative comme vous pouvez le remarquer sur l'image suivante : nous voyons un pattern dû à l'imprécision de la méthode :

Cette même méthode est aussi la source de la ligne blanche qui apparaît sur certains objets tessellés comme dans l'image suivante :

Récemment, j'ai trouvé dans l'un de mes tweets un lien sur l'article Followup : Normal MappingWithout Precomputed Tangents. L'article explique en détail les mathématiques à l'origine de l'espace Tangent Binormal Normal et à la fin de l'article, nous découvrons les fonctions magiques pour calculer en temps réel une normale perturbée à partir d'une texture de normales :

mat3 cotangent_frame(vec3 N, vec3 p, vec2 uv)

Et

vec3 perturb_normal(vec3 N, vec3 V, vec2 texcoord)

Soit, testons cela dans GLSL Hacker. J'ai rapidement codé une démonstration qui affiche un tore avec une texture de normales en utilisant les fonctions précédentes. Le code GLSL fonctionne parfaitement sous Windows (GTX 680 et pilotes R310.90), Mac OS X 10.8 (Geforce GT 650M ou Intel HD 4000) et Linux (Mint 13 avec GTX 680 et pilotes R313.18) :

Le mappage des normales fonctionne correctement et le résultat est beau. Je pense que je ne vais pas ajouter les vecteurs tangents précalculés dans GLSL Hacker pour le moment.

Vous pouvez trouver la démonstration pour GLSL Hacker dans le dossier GLSL_Normal_Mapping/ du pack d'exemples. La démonstration est disponible en trois versions : OpenGL 2.1, OpenGL 3.2 et OpenGL 4.2.

Voici le code complet GLSL 3.2 (vertex shader et pixel shader) qui effectue le normal mapping avec l'éclairage de Phong :

vertex shader

#version 150
in vec4 gxl3d_Position;
in vec4 gxl3d_Normal;
in vec4 gxl3d_TexCoord0;
out vec4 Vertex_UV;
out vec4 Vertex_Normal;
out vec4 Vertex_LightDir;
out vec4 Vertex_EyeVec;

// passée automatiquement par GLSL Hacker
uniform mat4 gxl3d_ModelViewProjectionMatrix;

// passée automatiquement par GLSL Hacker
uniform mat4 gxl3d_ModelViewMatrix;

uniform vec4 light_position;
uniform vec4 uv_tiling;

void main()
{
gl_Position = gxl3d_ModelViewProjectionMatrix * gxl3d_Position;
Vertex_UV = gxl3d_TexCoord0 * uv_tiling;
Vertex_Normal = gxl3d_ModelViewMatrix  * gxl3d_Normal;
vec4 view_vertex = gxl3d_ModelViewMatrix * gxl3d_Position;
Vertex_LightDir = light_position - view_vertex;
Vertex_EyeVec = -view_vertex;
}

fragment shader

#version 150
precision highp float;
uniform sampler2D tex0; // texture des couleurs
uniform sampler2D tex1; // texture des normales
uniform vec4 light_diffuse;
uniform vec4 material_diffuse;
uniform vec4 light_specular;
uniform vec4 material_specular;
uniform float material_shininess;
in vec4 Vertex_UV;
in vec4 Vertex_Normal;
in vec4 Vertex_LightDir;
in vec4 Vertex_EyeVec;
out vec4 Out_Color;

// http://www.thetenthplanet.de/archives/1180
mat3 cotangent_frame(vec3 N, vec3 p, vec2 uv)
{
// récupère les vecteurs du triangle composant le pixel
vec3 dp1 = dFdx( p );
vec3 dp2 = dFdy( p );
vec2 duv1 = dFdx( uv );
vec2 duv2 = dFdy( uv );

// résout le système linéaire
vec3 dp2perp = cross( dp2, N );
vec3 dp1perp = cross( N, dp1 );
vec3 T = dp2perp * duv1.x + dp1perp * duv2.x;
vec3 B = dp2perp * duv1.y + dp1perp * duv2.y;

// construit une trame invariante à l'échelle
float invmax = inversesqrt( max( dot(T,T), dot(B,B) ) );
return mat3( T * invmax, B * invmax, N );
}

vec3 perturb_normal( vec3 N, vec3 V, vec2 texcoord )
{
// N, la normale interpolée et
// V, le vecteur vue (vertex dirigé vers l'œil)
vec3 map = texture(tex1, texcoord ).xyz;
map = map * 255./127. - 128./127.;
mat3 TBN = cotangent_frame(N, -V, texcoord);
return normalize(TBN * map);
}

void main()
{
vec2 uv = Vertex_UV.xy;

vec3 N = normalize(Vertex_Normal.xyz);
vec3 L = normalize(Vertex_LightDir.xyz);
vec3 V = normalize(Vertex_EyeVec.xyz);
vec3 PN = perturb_normal(N, V, uv);

vec4 tex01_color = texture(tex0, uv).rgba;
vec4 final_color = vec4(0.2, 0.15, 0.15, 1.0) * tex01_color;

float lambertTerm = dot(PN, L);
if (lambertTerm > 0.0)
{
final_color += light_diffuse * material_diffuse * lambertTerm * tex01_color;

vec3 E = normalize(Vertex_EyeVec.xyz);
vec3 R = reflect(-L, PN);
float specular = pow( max(dot(R, E), 0.0), material_shininess);
final_color += light_specular * material_specular * specular;
}
Out_Color.rgb = final_color.rgb;
//Out_Color.rgb = PN.rgb;
//Out_Color.rgb = N.rgb;
Out_Color.a = 1.0;
}

II - Remerciements

Cet article est une traduction autorisée de l'article paru sur Geeks3D.com.

Je tiens à remercier Winjerome et dourouc05 pour leur relecture lors de la traduction de cet article, ainsi que ClaudeLELOUP pour sa relecture orthographique.