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 :
#
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;
}
#
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.