Mapeo de Sombras

Anteriormente veíamos como dar efectos de iluminación a las escenas 3D, para completar un efecto realista en nuestras escenas debemos agregar sombras, ya en el mundo real siempre que tengamos luz se genera una sombra, aprenderemos a utilizar el Frame Buffer Object (FBO) para almacenar un mapa de profundidad que será utilizado para crear la sombra de los objetos en la escena 3D.

Esta técnica se divide en dos pasos:

Construir el mapa de profundidad desde el punto de vista de la fuente de luz, utilizamos el FBO para almacenar el mapa de profundidad en la textura indicada.

Usar la información almacenada en el mapa de profundidad para crear la sombra, se calcula la distancia del punto a la cámara y se compara con la distancia almacenada en el mapa de profundidad, si la distancia es mayor, el punto se encuentra en sombra.

depth map aprender opengl
La imagen de la izquierda muestra como se obtiene la profundidad desde el punto de vista de la fuente de luz para una serie de puntos, la punta de la flecha indica el punto donde se obtiene la profundidad, aquellos puntos que no sean visibles para la fuente de luz están en sombra.

En la imagen derecha de observa el punto Pb, el cual visto desde el punto de vista de la cámara y la fuente de luz tiene la misma profundidad, no está en sombra, el punto Pa es visible desde la cámara pero no desde la fuente de luz por lo que esta en sombra, la profundidad medida desde la cámara es mayor a la obtenida desde la fuente de luz.

Generar Mapa de Profundidad


Lo primero que requerimos es crear un FBO y asociarlo a una textura, trabajamos con la textura del mismo modo que lo hemos venido haciendo en tutoriales anteriores, solo debemos tener en cuenta que el contenido de la textura será establecido con la información de profundidad de la escena.

glGenTextures(1, &shadowMapTexID);
glBindTexture(GL_TEXTURE_2D, shadowMapTexID);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);

static GLfloat border[4] = { 1, 0, 0, 0 };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border);

glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH_COMPONENT32, SHADOW_MAP_SIZE, SHADOW_MAP_SIZE);

glGenFramebuffers(1, &fboID);
glBindFramebuffer(GL_FRAMEBUFFER, fboID);

glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, shadowMapTexID, 0);

glDrawBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);

GL_DEPTH_COMPONENT32 nos permite crear una textura que será usada para almacenar la información de profundidad, SHADOW_MAP_SIZE establece el tamaño de la textura, utilizar un valor demasiado grande consumirá mucha memoria, un valor pequeño no proporcionará un buen resultado visual.

Lo siguiente que debemos hacer es renderizar la escena desde el punto de vista de la fuente de luz, requerimos las matrices de visualización y proyección para la fuente de luz, las creamos de manera similar a como lo hicimos anteriormente con las cámaras.

glm::mat4 P_L = glm::frustum(-1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 100.0f);
glm::mat4 MV_L = glm::lookAt(lightPosOS, glm::vec3(0), glm::vec3(0, 1, 0));

glUniformMatrix4fv(shadow_shader.getUniformLocation("MVP"), 1, GL_FALSE, glm::value_ptr(P_L * MV_L));

glBindFramebuffer(GL_FRAMEBUFFER, fboID);
glViewport(0, 0, SHADOW_MAP_SIZE, SHADOW_MAP_SIZE);
glClear(GL_DEPTH_BUFFER_BIT);

model.draw();

glBindFramebuffer(GL_FRAMEBUFFER, 0);

Las shaders son bastante simples, el vertex shader solo transforma la posición multiplicando por la matriz MVP de la fuente de luz, el fragment shader solo está presente porque OpenGL lo requiere.

//---------------- Vertex Shader ------------------

uniform mat4 MVP;
layout (location = 0) in vec3 position;

void main(void)
{
       gl_Position = MVP * vec4(position, 1.0);
}

//------------- Fragment Shader -----------------

layout (location = 0) out vec4 color;

void main(void)
{
       color = vec4(1.0);
}

Antes de renderizar activamos el FBO y establecemos el tamaño del viewport al mismo de la textura, limpiamos el buffer de profundidad glClear(GL_DEPTH_BUFFER_BIT).

Para visualizar el mapa de profundidad crearemos un cuadro 2D sobre el cual aplicaremos la textura, recordamos restablecer el tamaño del viewport, creamos un par de shaders llamados shadow-map-debug, estos solo aplican la textura a la figura.

mapa de profundidad

Generar Sombras


Generamos la escena del mismo modo que lo hemos venido haciendo, solo requerimos la matriz de sombra que llamaremos S, la misma se obtiene multiplicando las matrices B, P_L, MV_L, donde la matriz B en una matriz 4x4 usada para transformar los valores de profundidad del rango [-1, +1] a [0, 1].

glm::mat4 B(0.5, 0.0, 0.0, 0.0,
            0.0, 0.5, 0.0, 0.0,
            0.0, 0.0, 0.5, 0.0,
            0.5, 0.5, 0.5, 1.0);

glm::mat4 S = B * P_L * MV_L;

glm::mat4 M = glm::mat4(1);
glm::mat4 MV = camera.getViewMatrix() * M;
glm::mat4 MVP = camera.getProjectionMatrix() * MV;
glm::mat3 N = glm::inverseTranspose(glm::mat3(MV));

glUniformMatrix4fv(light_shadow_shader.getUniformLocation("MVP"), 1, GL_FALSE, glm::value_ptr(MVP));
glUniformMatrix4fv(light_shadow_shader.getUniformLocation("MV"), 1, GL_FALSE, glm::value_ptr(MV));
glUniformMatrix4fv(light_shadow_shader.getUniformLocation("M"), 1, GL_FALSE, glm::value_ptr(M));
glUniformMatrix4fv(light_shadow_shader.getUniformLocation("S"), 1, GL_FALSE, glm::value_ptr(S));
glUniformMatrix3fv(light_shadow_shader.getUniformLocation("N"), 1, GL_FALSE, glm::value_ptr(N));

glm::vec3 lightPosES = glm::vec3(MV * glm::vec4(lightPosOS, 0));

glUniform3fv(light_shadow_shader.getUniformLocation("light_position"), 1, glm::value_ptr(lightPosES));
glUniform3fv(light_shadow_shader.getUniformLocation("diffuse_color"), 1, glm::value_ptr(glm::vec3(0.75f)));

model.draw();

El en fragment shader usamos la función textureProj() para determinar si el fragmenta está o no en sombra, esta función devuelve 1.0 en caso positivo, 0.0 de caso contrario, la función usa un tipo sampler2DShadow.

void main() {
 vec3 L = normalize(light_position - POSITION);

 float diffuse = max(0, dot(NORMAL, L));
 
 if(SHADOW.w > 1) {
   float shadow = textureProj(shadowMap, SHADOW);
   diffuse = mix(diffuse, diffuse * shadow, 0.5);
 } 
 
 color = diffuse * vec4(diffuse_color, 1);
}

La función mix es usada para mesclar la sombra con la componente difusa, el resultado final es el siguiente:

sombras opengl
Para reducir el efecto shadow acne usamos la funciones opengl glCullFace(GL_FRONT) antes de generar el mapa de profundidad y la función glPolygonOffset(1.0f, 1.0f).

GitHub: Mapeo de Sombras

Comentarios

Temas relacionados

Entradas populares de este blog

tkinter Grid

tkinter Canvas

Histogramas OpenCV Python

Python Binance API