I. Tangent Binormal Normal

Image non disponible

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 :

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

Image non disponible

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 :

Image non disponible

Récemment, j'ai trouvé dans l'un de mes tweets un lien sur l'article Followup : Normal Mapping Without 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 :

 
Sélectionnez
mat3 cotangent_frame(vec3 N, vec3 p, vec2 uv)

Et

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

Image non disponible
Image non disponible
Image non disponible
Image non disponible

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