Normal Map

Un normal map es una textura que es usada para almacenar las coordenadas del vector normal, una textura regularmente almacena las componentes de color RGB, en un normal map se almacenaran las coordenadas XYZ que serán utilizadas para definir el vector normal, a esta técnica se le conoce como: normal mapping o en ocasiones como bump mapping.

Cuando deseamos incrementar el nivel de detalles de una figura requerimos agregar más vértices, cuanto más detallada deseemos la figura mayor cantidad de vértices tendremos, esto se traduce en una baja en el rendimiento de la aplicación y mayor uso de memoria, por ello podemos recurrir a los normal map para incrementar el detalle de nuestros modelos 3D sin perjudicar el rendimiento.

cyborg_normal
Este es el normal map definido para el modelo cyborg que utilizamos en el tutorial OpenGL anterior: Diffuse y Specular Maps

Utilizaremos el sistema de coordenadas referente al espacio de textura, definido por los vectores T (tangent), B (bitanget) y N (normal), normalmente llamado tangent space o texture space, utilizaremos estos 3 vectores para crear la matriz TBN.

normal map tutorial opengl moderno
La biblioteca assimp genera los vectores T y B por nosotros, para indicarle que realice esta operación activamos el flag aiProcess_CalcTangentSpace en el métodos ReadFile(), obtenemos cada uno de los vectores y los almacenamos en un buffer del mismo modo que lo hicimos con los vectores normales.

if (mesh->HasTangentsAndBitangents()) {
    const aiVector3D* t = &(mesh->mTangents[i]);
    const aiVector3D* b = &(mesh->mBitangents[i]);

    tangent.push_back(glm::vec3(t->x, t->y, t->z));  
    bitangent.push_back(glm::vec3(b->x, b->y, b->z));
}

También requerimos que assimp cargue la textura correspondiente al normal map, la cargamos del mismo modo que usamos para las texturas difusa y especular, load_material(mesh, aiTextureType_HEIGHT, texture_normal), esta textura es identificada por aiTextureType_HEIGHT.

El vertex shader transforma los vectores T, B, N y los envía al fragment shader:

layout (location = 0) in vec3 position;
layout (location = 1) in vec2 texture;
layout (location = 2) in vec3 normal;
layout (location = 3) in vec3 tangent;
layout (location = 4) in vec3 bitangent;

uniform mat4 mvp_matrix, mv_matrix;
uniform mat3 n_matrix;

out vec3 V1;
out vec2 UV;
out vec3 T, B, N;

void main(void)
{
 vec4 posW  = mv_matrix * vec4(position, 1.0);

 T = n_matrix * tangent;
 B = n_matrix * bitangent; 
 N = n_matrix * normal;

 UV = texture;
 V1 = -posW.xyz;

 gl_Position = mvp_matrix * vec4(position, 1.0);
}

La función texture(texture, coordenada) nos devuelve un rango de valores entre [0, 1] que representa los colores en los canales RGB, para trabajar los cálculos de iluminación requerimos un rango de valores entre [-1, 1], para transformar de un rango de valores a otro aplicamos la siguiente fórmula: x * 2.0 - 1.0, necesitamos crear la matriz TBN para transformar la normal en el mismo espacio de coordenadas usados para el cálculo de iluminación.

void main()
{
 vec3 normalT = texture(material.normal, UV).rgb;
   normalT = normalT * 2.0 - vec3(1.0);

 mat3 TBN = mat3(normalize(T), normalize(B), normalize(N));

 vec3 NT = normal_off ? N : TBN * normalT;
   NT = normalize(NT);

 vec3 L = normalize(light.direction);
 vec3 V = normalize(V1);
 vec3 H = normalize(L + V); 

 float diff = max(dot(NT, L), 0);
 float spec = pow(max(dot(NT, H), 0), material.shininess) * material.shininess_strength;

 vec3 texture_ambient  = texture_off ? vec3(0.10) : texture(material.ambient , UV).rgb;
 vec3 texture_diffuse  = texture_off ? vec3(0.64) : texture(material.diffuse , UV).rgb;
 vec3 texture_specular = texture_off ? vec3(0.50) : texture(material.specular, UV).rgb;

 vec3 ambient  = texture_ambient  * light.ambient;
 vec3 diffuse  = texture_diffuse  * light.diffuse  * diff;
 vec3 specular = texture_specular * light.specular * spec;

 color = vec4(ambient + diffuse + specular, 1.0);
}

Este fragment shader es similar al que usamos en tutoriales anteriores, varía solo que la normal es calculada a partir de la textura, se agregaron también un par de condicionales para activar o desactivar el uso del normal map y el uso de texturas, con la tecla T se puede activar/desactivar las texturas, y con la tecla N se activa/desactiva el normal map.

opengl normal map off
opengl normal map on
En la primera imagen se muestra el modelo 3D cyborg.obj con el normal map inactivo, la segunda muestra el mismo modelo ahora con el normal map activado, usamos la tecla N, las texturas difusa y especular están inactivas, tecla T.

normal-difuse-especula
difuse-normal-especular
El mismo modelo ahora con las texturas difusa y especular activadas, la primera imagen sin el normal map y la segunda con el normal map activo.

GitHub: OpenGL Normal Maps

Comentarios

Entradas populares de este blog

Conectar SQL Server con Java

Acceso a la webcam con OpenCV

Procesamiento de imágenes en OpenCV

Entrenar OpenCV en Detección de Objetos

Analizador Léxico