|
Grado en Ingeniería Informática
Realidad Virtual
Curso 2023/2024
|
Práctica 11
|
|
Mapas de sombras
|
|
Objetivos
|
|
El objetivo de esta práctica es incluir sombras en las escenas por medio de la técnica conocida
como ShadowMap. La práctica consta de tres proyectos. El primero desarrolla el método ShadowMap
básico. El segundo proyecto añade la técnica de percentage-closer filtering (PCF) como
técnica de antialiasing. El tercer método desarrolla una técnica de antialiasing aleatoria.
|
|
Framebuffers
|
|
El proceso de renderizado genera como salida la imagen procesada almacenada en un array de píxeles que denominamos
ColorBuffer.
El renderizado genera también otros buffers paralelos destinados a almacenar los valores de profundidad (DepthBuffer) y
otros valores auxiliares (StencilBuffer). Por tanto, la estructura completa de la salida del renderizado está formado por esos
tres buffers. Esta estructura se conoce como FrameBuffer.
OpenGL crea un FrameBuffer por defecto que asocia a la pantalla y
que tiene preasignado el identificador 0. OpenGL permite además al
programador construir sus propios FrameBuffers y dirigir el
proceso de renderizado hacia ellos. Para crear un FrameBuffer se
utiliza el comando glGenFramebuffers(), para activarlo y
dirigir el renderizado hacia él se utiliza el comando
glBindFramebuffer(), para eliminarlo se utiliza el comando
glDeleteFramebuffers().
Al crear un framebuffer se obtiene un identificador
numérico, pero la creación de los buffers que lo forman es
responsabilidad del programador. Los buffers que pueden asociarse a
un framebuffer pueden ser de dos tipos: texturas y buffers
de renderizado (RenderBuffers). Para asignar una textura 2D
a uno de los buffers del framebuffer activo se utiliza el
comando glFramebufferTexture2D(). En este comando se indica
el buffer a asignar, la textura a asociar y el nivel de mipmap. Para
asignar un renderbuffer a uno de los buffers del
framebuffer activo se utiliza el comando
glFramebufferRenderbuffer(). En este comando se indica el
buffer a asignar y el renderbuffer a asociar.
Para asignar el contenido a los buffers que forman el framebuffer se
utilizan las siguientes constantes: GL_COLOR_ATTACHMENT0 (ColorBuffer),
GL_DEPTH_ATTACHMENT (DepthBuffer), GL_STENCIL_ATTACHMENT (StencilBuffer).
Si el FragmentShader define varias salidas el
framebuffer correspondiente debe tener un colorbuffer
para cada una de esas salidad. Esos buffers se identifican como
GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2,
...
Para crear un buffer de renderizado se utiliza el comando
glGenRenderbuffers(), para activarlo se utiliza el comando
glBindRenderbuffer() y para borralo se utilza el comando
glDeleteRenderbuffers(). El tamaño del buffer se asigna con el
comando glRenderbufferStorage() donde se indica la anchura
y altura, así como el formato a utilizar en el buffer.
Al activar un framebuffer el proceso de renderizado se
dirige hacia esa estructura. Es importante asignar el tamaño del
Viewport
correspondiente al framebuffer activo antes de lanzar los
comandos de dibujo. También se puede indicar cuales son los
colorbuffers a los que se dirigirá el renderizado utilizando el
comando glDrawBuffers().
|
|
Subrutinas
|
|
Una subrutina es un mecanismo que permite seleccionar una función
(entre un conjunto de funciones posibles) basado en el valor de una
variable uniforme. Las subrutinas proporcionan una forma de
seleccionar implementaciones alternativas en tiempo de ejecución sin
necesidad de cambiar el programa GLSL.
Para declarar una subrutina en un shader se utiliza la instrucción
subroutine seguida de la declaración del tipo de
función. Esta declaración define el nombre de la subrutina, los
argumentos de la función y el tipo de dato devuelto.
subroutine vec3 shadeModelType( vec4 position, vec3 normal);
|
Para declarar una variable uniforme que contenga un puntero a una
subrutina se utiliza la instrucción subroutine uniform.
subroutine uniform shadeModelType shadeModel;
|
Para declarar una función que desarrolla una subrutina se utiliza el
modificador subroutine( nombre ) seguida de la
declaración de la función.
subroutine( shadeModelType )
vec3 phongModel( vec4 position, vec3 norm )
{
…
}
subroutine( shadeModelType )
vec3 diffuseOnly( vec4 position, vec3 norm )
{
…
}
|
Para usar una subrutina se utiliza la variable uniforme como si
fuera una función.
void main()
{
…
LightIntensity = shadeModel(eyePosition,eyeNorm);
…
}
|
|
|
ShadowMaps
|
|
La técnica básica de ShadowMap consiste en generar la imagen en dos
pasadas. En la primera pasada se construye una imagen de la escena
desde el punto de vista de la luz. De esta forma se consigue
almacenar la distancia de cada punto iluminado hasta el foco de luz
(esta es la información que se almacena en el DepthBuffer en el
proceso de renderizado). En la segunda pasada se genera la imagen
desde el punto de vista de la cámara. Para cada punto a generar de
esta imagen se calcula su distancia a la luz y su posición en la
imagen tomada desde la luz. Con esta segunda información se puede
conocer la distacia al foco asociada a ese punto. Si la distancia
almacenada es menor que la distancia del punto entonces en punto se
encuentra en sombra. En caso contrario el punto se encuentra en una
zona iluminada.

Para generar la imagen desde el punto de vista de la luz se utiliza
un Frame Buffer Object (FBO) distinto al de la imagen a mostrar en
pantalla. Este FBO tan solo necesita configurar el DepthBuffer ya
que la única información relevante a almacenar es la profundidad de
cada punto de la imagen. Esto requiere incluir algunas
modificaciones en el modelo a representar (CGModel). Por un lado es
necesario añadir a la clase CGModel dos nuevos campos para almacenar
la referencia al FBO y al DepthBuffer. Por otro lado es necesario
incluir un nuevo método para inicializar estos componentes (
InitShadowMap() ). Por último hay que modificar el
método de renderizado para dividirlo en dos pasadas.
class CGModel {
public:
void initialize(int w, int h);
void finalize();
void render();
void update();
void key_pressed(int key);
void mouse_button(int button, int action);
void mouse_move(double xpos, double ypos);
void resize(int w, int h);
private:
CGShaderProgram* sceneProgram;
CGShaderProgram* skyboxProgram;
CGScene* scene;
CGCamera* camera;
CGSkybox* skybox;
glm::mat4 projection;
GLsizei wndWidth;
GLsizei wndHeight;
GLuint shadowFBO;
GLuint depthTexId;
bool InitShadowMap();
void CameraConstraints();
};
//
// FUNCIÓN: CGModel::initialize(int, int)
//
// PROPÓSITO: Initializa el modelo 3D
//
void CGModel::initialize(int w, int h)
{
// Crea el programa gráfico para la escena
sceneProgram = new CGShaderProgram(IDR_SHADER1, IDR_SHADER2, -1, -1, -1);
if (sceneProgram->IsLinked() == GL_FALSE) return;
// Crea el programa gráfico para el entorno
skyboxProgram = new CGShaderProgram(IDR_SHADER3, IDR_SHADER4, -1, -1, -1);
if (skyboxProgram->IsLinked() == GL_FALSE) return;
// Crea la cámara
camera = new CGCamera();
camera->SetPosition(0.0f, 5.0f, 30.0f);
// Crea el skybox
skybox = new CGSkybox();
// Crea la escena
scene = new CGScene();
// Crea el Framebuffer de la sombra
bool frameBufferStatus = InitShadowMap();
if (!frameBufferStatus) return;
// Asigna el viewport y el clipping volume
resize(w, h);
// Opciones de dibujo
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
}
//
// FUNCIÓN: CGModel::InitShadowMap()
//
// PROPÓSITO: Inicializa el FBO para almacenar la textura de sombra
//
bool CGModel::InitShadowMap()
{
GLfloat border[] = { 1.0f, 1.0f, 1.0f, 1.0f };
GLsizei shadowMapWidth = 1024;
GLsizei shadowMapHeight = 1024;
glGenFramebuffers(1, &shadowFBO);
glBindFramebuffer(GL_FRAMEBUFFER, shadowFBO);
glGenTextures(1, &depthTexId);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, depthTexId);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, shadowMapWidth,
shadowMapHeight, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
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);
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthTexId, 0);
glDrawBuffer(GL_NONE);
bool result = true;
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{ result = false; }
glBindFramebuffer(GL_FRAMEBUFFER, 0);
return result;
}
//
// FUNCIÓN: CGModel::render()
//
// PROPÓSITO: Genera la imagen
//
void CGModel::render()
{
//*********************************************************//
// Genera el ShadowMap //
//*********************************************************//
// Activa el programa de la escena
sceneProgram->Use();
// Asigna las matrices Viewport, View y Projection de la luz.
glm::mat4 lightViewMatrix = scene->GetLightViewMatrix();
glm::mat4 lightPerspective = glm::ortho(-150.0f, 150.0f,
-150.0f, 150.0f, 0.0f, 400.0f);
glm::mat4 lightMVP = lightPerspective * lightViewMatrix;
// Activa el framebuffer de la sombra
glBindFramebuffer(GL_FRAMEBUFFER, shadowFBO);
// Limpia la información de profundidad
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Selecciona la subrutina recordDepth
sceneProgram->SetUniformSubroutine(GL_FRAGMENT_SHADER, "recordDepth");
// Activa front-face culling
glCullFace(GL_FRONT);
//Asigna el viewport
glViewport(0, 0, 1024, 1024);
// Dibuja la escena
scene->Draw(sceneProgram, lightPerspective, lightViewMatrix, lightMVP);
//*********************************************************//
// Dibuja el skybox //
//*********************************************************//
// Activa el framebuffer de la imagen
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// Limpia el framebuffer
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Activa back-face culling
glCullFace(GL_BACK);
// Asigna el viewport
glViewport(0, 0, wndWidth, wndHeight);
// Dibuja el skybox
glm::mat4 view = camera->ViewMatrix();
skyboxProgram->Use();
skybox->Draw(skyboxProgram, projection, view);
//*********************************************************//
// Dibuja la escena //
//*********************************************************//
// Activa el programa de la escena
sceneProgram->Use();
// Selecciona la subrutina shadeWithShadow
sceneProgram->SetUniformSubroutine(GL_FRAGMENT_SHADER, "shadeWithShadow");
sceneProgram->SetUniformI("ShadowMap", 1);
// Dibuja la escena
glm::mat4 viewMatrix = camera->ViewMatrix();
scene->Draw(sceneProgram, projection, viewMatrix, lightMVP);
}
|
El VertexShader requiere generar una salida adicional que será utilizada
en el FragmentShader en la segunda pasada del renderizado. Se trata de
la posición de cada vértice en coordenadas clip en la imagen tomada
desde la luz (ShadowCoord). Para generar esta salida hace falta también
una nueva variable uniforme (ShadowMatrix) que contiene la matriz
de transformación MVP desde la luz.
#version 400
layout(location = 0) in vec3 VertexPosition;
layout(location = 1) in vec3 VertexNormal;
layout(location = 2) in vec2 VertexTexCoord;
uniform mat4 MVP;
uniform mat4 ViewMatrix;
uniform mat4 ModelViewMatrix;
uniform mat4 ShadowMatrix;
out vec3 Position;
out vec3 Normal;
out vec2 TexCoord;
out vec4 ShadowCoord;
void main()
{
vec4 n4 = ModelViewMatrix*vec4(VertexNormal, 0.0);
vec4 v4 = ModelViewMatrix*vec4(VertexPosition, 1.0);
Normal = vec3(n4);
Position = vec3(v4);
TexCoord = VertexTexCoord;
ShadowCoord = ShadowMatrix * vec4(VertexPosition, 1.0);
gl_Position = MVP * vec4(VertexPosition, 1.0);
}
|
El FragmentShader tiene comportamientos muy diferentes en cada pasada. En la primera pasada
ni siquiera se calcula el color de los puntos de la imagen sino que solo se almacena la
profundidad. Por tanto, en la primera pasada no hay que hacer nada ya que la profundidad
se almacena automáticamente en el proceso de renderizado (función
recordDepth() ). En la segunda pasada hay que
calcular las coordenadas de la textura ShadowMap a partir de las coordenadas clip
de la imagen de la luz (las coordenadas clip se definen entre -1 y +1 mientras que las
coordenadas de textura tienen un rango de 0 a 1). Con estas
coordenadas se puede calcular la profundidad almacenada en la
textura y compararla con la profundidad correspondiente al píxel
(componente Z de las coordenadas clip). Si la profundidad del pixel
es mayor que la almacenada en la textura el pixel se encuentra en
sombra (función shadeWithShadow()). El color del pixel se calcula con el modelo de Phong
tradicional (función ads()), pero en caso de sombra solo se utiliza la componente de
luz ambiental.
Para poder diferenciar el comportamiento en cada pasada se utiliza
la subrutina RenderPassType. Tanto la función
recordDepth() como la función shadeWithShadow()
cumplen con esta subrutina. El FragmentShader incluye la variable
uniforme RenderPass para almacenar la subrutina escogida,
de manera que el método main() se limita a ejecutar la
función referenciada en esta variable.
#version 400
in vec3 Position;
in vec3 Normal;
in vec2 TexCoord;
in vec4 ShadowCoord;
uniform sampler2D BaseTex;
uniform sampler2D ShadowMap;
uniform mat4 ViewMatrix;
struct LightInfo {
vec3 Ldir;
vec3 La;
vec3 Ld;
vec3 Ls;
};
uniform LightInfo Light;
struct MaterialInfo{
vec3 Ka;
vec3 Kd;
vec3 Ks;
float Shininess;
};
uniform MaterialInfo Material;
out vec4 FragColor;
vec3 ads(vec3 TexColor)
{
vec4 s4 = ViewMatrix*vec4(Light.Ldir, 0.0);
vec3 n = normalize(Normal);
vec3 v = normalize(-Position);
vec3 s = normalize(-vec3(s4));
vec3 r = reflect(-s, n);
float dRate = max(dot(s, n), 0.0);
float sRate = pow(max(dot(r, v), 0.0), Material.Shininess);
vec3 difusse = Light.Ld * Material.Kd * dRate;
vec3 specular = Light.Ls * Material.Ks * sRate;
return difusse*TexColor + specular;
}
subroutine void RenderPassType();
subroutine uniform RenderPassType RenderPass;
subroutine (RenderPassType)
void shadeWithShadow()
{
vec3 TexColor = vec3( texture(BaseTex,TexCoord) );
vec3 ambient = Light.La * Material.Ka * TexColor;
vec3 diffAndSpec = ads(TexColor);
float ShadowCoordX = (ShadowCoord.x/ShadowCoord.w) * 0.5 + 0.5;
float ShadowCoordY = (ShadowCoord.y/ShadowCoord.w) * 0.5 + 0.5;
float ShadowCoordZ = (ShadowCoord.z/ShadowCoord.w) * 0.5 + 0.5;
float shadowDepth = (texture(ShadowMap, vec2(ShadowCoordX,ShadowCoordY))).z;
float shadow = 1.0;
if(shadowDepth < ShadowCoordZ) shadow = 0;
FragColor = vec4(shadow * diffAndSpec+ ambient, 1.0);
}
subroutine (RenderPassType)
void recordDepth()
{
}
void main()
{
RenderPass();
}
|
Para completar el proyecto es necesario incorporar algunas
modificaciones a las clases que hemos desarrollado en las prácticas
anteriores:
-
La
clase CGShaderProgram necesita dos métodos adicionales
para asignar variables uniformes de tipo subrutina (
SetVertexShaderUniformSubroutine() y
SetFragmentShaderUniformSubroutine() ).
-
La clase CGFigure se ha modificado para incluir una
referencia al material y para incorporar la variable
shadowViewMatrix en el método Draw().
-
Se ha añadido el método GetLightDirection() a la clase
CGLight
para acceder a la dirección de la luz.
-
En la clase CGScene se ha modificado el constructor
para definir las figuras por medio de objetos CGMaterial,
se ha añadido la variable shadowViewMatrix en el método
Draw() y se ha incluido al método
GetLightViewMatrix() para obtener la matriz de
posicionamiento de la luz.
A
continuación se muestra una captura del resultado.

|
|
Percentage-closer filtering
(PCF)
|
|
Uno de los problemas más importantes que tiene la técnica de
ShadowMap básica es que el pixelado de las sombras es muy acusado
(aliasing). En la siguiente imagen, en la que los objetos se han
dibujado sin textura, se puede apreciar claramente como
la sombra del cubo no corresponde a la proyección de una linea
recta, sino que la sombra aparece escalonada.

Para evitar este problema se han propuesto numerosas técnicas de
antialiasing. Una técnica sencilla para suavizar el pixelado
consiste en comparar el grado de sobra de cada pixel con el de los
píxeles vecinos de manera que las fronteras de la sombra se puedan
se hagan más borrosas. Esta técnnica se conoce como
percentage-closer filtering (PCF).
Para implementar esta técnica vamos a utilizar en los shaders un
tipo especial de variable denominada sampler2DShadow. La
diferencia fundamental entre el tipo de dato sampler2D y
sampler2DShadow es la forma en la que se accede a la
información de la textura. La función texture() sobre un
dato sampler2D utiliza una coordenada de textura de dos
dimensiones y devuelve el color almacenado en esas coordenadas de
textura (o una interpolación lineal con los texels vecinos). La
función texture() sobre un dato sampler2DShadow
utiliza una coordenada de textura de tres dimensiones. Las dos
primeras dimensiones permiten acceder al valor almacenado en esas
coordenadas de textura pero, a continuación, se compara el valor
almacenado con el valor de la tercera coordenada. Si el valor
almacenado describe la profundidad (ShadowMap) y lo comparamos con
la coordenada Z de las coordenadas de textura podemos saber en un
único paso si el punto está iluminado o en sombra.
La forma en la que se realiza la comparación se puede configurar por
medio de la propiedad GL_TEXTURE_COMPARE_MODE. El valor
GL_COMPARE_REF_TO_TEXTURE indica que la comparación se realizará
entre el valor de la textura y la tercera dimensión de la coordenada
de búsqueda. La función utilizada en la comparación se configura con
la propiedad GL_TEXTURE_COMPARE_FUNC. Si se escoge la función
GL_LESS la llamada a texture() devuelverá 1.0 cuando la
profundidad almacenada en el ShadowMap sea menor que la profundidad
del pixel estudiado. Si además se utiliza un filtrado lineal, el
resultado de texture() se calculará como una interpolación
lineal del resultado de la comparación, generando por tanto un valor
entre 0.0 y 1.0.
Para desarrollar la técnica PCF
hay que estudiar el grado de sombra de los texels vecinos. Para
ello, en vez de utilizar la función texture() hay que
utilizar la función textureProjOffset(). Esta función
permite incluir un desplazamiento (medido en texels) en las
coordenadas de textura. Sumando el efecto del filtrado lineal con la
búsqueda desplazada en cuatro direcciones se consigue una
interpolación entre 16 texels.

La implementación de la técnica básica de ShadowMap que se presentó
en el apartado anterior utilizaba un único programa gráfico tanto
para generar el mapa de sombra (en la primera pasada) como para
generar la imagen final (en la segunda pasada). Para diferenciar los
dos comportamientos se utilizaron subrutinas. En este caso la
implementación que vamos a presentar está basada en dos programas
gráficos diferentes (uno para generar el mapa de sombras y otro para
generar la imagen).
El programa gráfico dedicado a generar el mapa de sombras utiliza un
VertexShader muy sencillo en el que solo se tiene en cuenta la
posición de los vértices y se proyectan mediante una matriz MVP que
debe estar calculada desde el punto de vista de la luz.
#version 400
layout(location = 0) in vec3 VertexPosition;
uniform mat4 MVP;
void main()
{
gl_Position = MVP * vec4(VertexPosition, 1.0);
}
|
El FragmentShader de este programa gráfico es aún más simple ya que
solo tiene que almacenar la profundidad de cada fragmento y eso se
hace por defecto.
#version 400
void main()
{
}
|
El programa gráfico dedicado a generar la imagen utilizando la
ténica PCF utiliza un VertexShader
similar al utilizado en el
ShadowMap básico.
#version 400
layout(location = 0) in vec3 VertexPosition;
layout(location = 1) in vec3 VertexNormal;
layout(location = 2) in vec2 VertexTexCoord;
uniform mat4 MVP;
uniform mat4 ViewMatrix;
uniform mat4 ModelViewMatrix;
uniform mat4 ShadowMatrix;
out vec3 Position;
out vec3 Normal;
out vec2 TexCoord;
out vec4 ShadowCoord;
void main()
{
vec4 n4 = ModelViewMatrix*vec4(VertexNormal, 0.0);
vec4 v4 = ModelViewMatrix*vec4(VertexPosition, 1.0);
Normal = vec3(n4);
Position = vec3(v4);
TexCoord = VertexTexCoord;
ShadowCoord = ShadowMatrix * vec4(VertexPosition, 1.0);
gl_Position = MVP * vec4(VertexPosition, 1.0);
}
|
Por su parte, el FragmentShader
que desarrolla la técnica PCF es el siguiente. La variable
ShadowMap es ahora de tipo sampler2DShadow y la
función shadeWithShadow() se ha modificado para realizar
cuatro llamadas a textureProjOffset() y realizar la media
entre ellas.
#version 400
in vec3 Position;
in vec3 Normal;
in vec2 TexCoord;
in vec4 ShadowCoord;
uniform sampler2D BaseTex;
uniform sampler2DShadow ShadowMap;
uniform mat4 ViewMatrix;
struct LightInfo {
vec3 Ldir;
vec3 La;
vec3 Ld;
vec3 Ls;
};
uniform LightInfo Light;
struct MaterialInfo{
vec3 Ka;
vec3 Kd;
vec3 Ks;
float Shininess;
};
uniform MaterialInfo Material;
out vec4 FragColor;
vec3 ads(vec3 TexColor)
{
vec4 s4 = ViewMatrix*vec4(Light.Ldir, 0.0);
vec3 n = normalize(Normal);
vec3 v = normalize(-Position);
vec3 s = normalize(-vec3(s4));
vec3 r = reflect(-s, n);
float dRate = max(dot(s, n), 0.0);
float sRate = pow(max(dot(r, v), 0.0), Material.Shininess);
vec3 difusse = Light.Ld * Material.Kd * dRate;
vec3 specular = Light.Ls * Material.Ks * sRate;
return difusse*TexColor + specular;
}
void main()
{
vec3 TexColor = vec3( texture(BaseTex,TexCoord) );
vec3 ambient = Light.La * Material.Ka * TexColor;
vec3 diffAndSpec = ads(TexColor);
vec4 ShadowTexCoord = (ShadowCoord/ShadowCoord.w)*0.5 + vec4(0.5, 0.5, 0.5, 0.5);
float sum = 0;
sum += textureProjOffset(ShadowMap, ShadowTexCoord, ivec2(-1,-1));
sum += textureProjOffset(ShadowMap, ShadowTexCoord, ivec2(-1,1));
sum += textureProjOffset(ShadowMap, ShadowTexCoord, ivec2(1,1));
sum += textureProjOffset(ShadowMap, ShadowTexCoord, ivec2(1,-1));
float shadow = sum * 0.25;
FragColor = vec4(shadow * diffAndSpec+ ambient, 1.0);
}
|
La clase CGFigure debe distinguir ahora entre dibujar el
mapa de sombra o dibujar la figura con normalidad. Para dibujar el
mapa de sombra se ha incluido un nuevo método llamado
drawShadow()
que utiliza como argumento la matriz View-Projection generada desde
el punto de vista de la luz.
//
// FUNCIÓN: CGFigure::DrawShadow(CGShaderProgram * program, glm::mat4 shadowMatrix)
//
// PROPÓSITO: Dibuja la sombra de la figura
//
void CGFigure::DrawShadow(CGShaderProgram* program, glm::mat4 shadowMatrix)
{
glm::mat4 mvp = shadowMatrix * location;
program->SetUniformMatrix4("MVP", mvp);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, numFaces * 3, GL_UNSIGNED_SHORT, NULL);
}
|
La clase CGModel se ha modificado para incluir tres
programas gráficos: el dedicado al fondo (skyboxProgram),
el dedicado a generar el mapa de sombra (shadowProgram) y
el dedicado a dibujar los objetos de la escena (sceneProgram).
La implementación de la técnica PCF necesita modificar la función
InitShadowMap() de la clase CGModel. El nuevo
contenido es el siguiente:
//
// FUNCIÓN: CGModel::initialize(int, int)
//
// PROPÓSITO: Initializa el modelo 3D
//
void CGModel::initialize(int w, int h)
{
// Crea el programa gráfico para el entorno
skyboxProgram = new CGShaderProgram(IDR_SHADER1, IDR_SHADER2, -1, -1, -1);
if (skyboxProgram->IsLinked() == GL_FALSE) return;
// Crea el programa gráfico para la sombra
shadowProgram = new CGShaderProgram(IDR_SHADER5, IDR_SHADER6, -1, -1, -1);
if (shadowProgram->IsLinked() == GL_FALSE) return;
// Crea el programa gráfico para la escena
sceneProgram = new CGShaderProgram(IDR_SHADER3, IDR_SHADER4, -1, -1, -1);
if (sceneProgram->IsLinked() == GL_FALSE) return;
// Crea la cámara
camera = new CGCamera();
camera->SetPosition(0.0f, 5.0f, 30.0f);
// Crea el skybox
skybox = new CGSkybox();
// Crea la escena
scene = new CGScene();
// Crea el Framebuffer de la sombra
bool frameBufferStatus = InitShadowMap();
if (!frameBufferStatus) return;
// Asigna el viewport y el clipping volume
resize(w, h);
// Opciones de dibujo
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
}
//
// FUNCIÓN: CGModel::InitShadowMap()
//
// PROPÓSITO: Inicializa el FBO para almacenar la textura de sombra
//
bool CGModel::InitShadowMap()
{
GLfloat border[] = { 1.0f, 1.0f, 1.0f, 1.0f };
GLsizei shadowMapWidth = 1024;
GLsizei shadowMapHeight = 1024;
glGenFramebuffers(1, &shadowFBO);
glBindFramebuffer(GL_FRAMEBUFFER, shadowFBO);
glGenTextures(1, &depthTexId);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, depthTexId);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, shadowMapWidth,
shadowMapHeight, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LESS);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
GL_TEXTURE_2D, depthTexId, 0);
glDrawBuffer(GL_NONE);
bool result = true;
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
result = false;
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
return result;
}
//
// FUNCIÓN: CGModel::render()
//
// PROPÓSITO: Genera la imagen
//
void CGModel::render()
{
//*********************************************************//
// Genera el ShadowMap //
//*********************************************************//
// Activa el framebuffer de la sombra
glBindFramebuffer(GL_FRAMEBUFFER, shadowFBO);
// Activa el programa que genera el shadowmap
shadowProgram->Use();
// Asigna las matrices Viewport, View y Projection de la luz.
glm::mat4 lightViewMatrix = scene->GetLightViewMatrix();
glm::mat4 lightPerspective = glm::ortho(-150.0f, 150.0f,
-150.0f, 150.0f, 0.0f, 400.0f);
glm::mat4 lightMVP = lightPerspective * lightViewMatrix;
// Limpia la información de profundidad
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Activa front-face culling
glCullFace(GL_FRONT);
//Asigna el viewport
glViewport(0, 0, 1024, 1024);
// Dibuja la escena
scene->DrawShadow(shadowProgram, lightMVP);
//*********************************************************//
// Dibuja el skybox //
//*********************************************************//
// Activa el framebuffer de la imagen
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// Limpia el framebuffer
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Activa back-face culling
glCullFace(GL_BACK);
// Asigna el viewport
glViewport(0, 0, wndWidth, wndHeight);
// Dibuja el skybox
glm::mat4 view = camera->ViewMatrix();
skyboxProgram->Use();
skybox->Draw(skyboxProgram, projection, view);
//*********************************************************//
// Dibuja la escena //
//*********************************************************//
// Activa el programa de la escena
sceneProgram->Use();
sceneProgram->SetUniformI("ShadowMap", 1);
// Dibuja la escena
glm::mat4 viewMatrix = camera->ViewMatrix();
scene->Draw(sceneProgram, projection, viewMatrix, lightMVP);
}
|
El resultado conseguido con esta técnica se puede apreciar en la siguiente captura.

|
|
Random Sampling
|
|
La técnica de PCF suaviza un poco el pixelado, pero la frontera de
la sombra sigue siendo escalonada. Para suavizar aun más esta
frontera habría que hacer una interpolación con un mayor numero de
píxeles vecinos. El problema de la técnica PCF en este caso es que
la interpolación se realiza tanto en los píxeles de la frontera de
la sombra como en el interior de la zona sombreada o iluminada, de
manera que aumentar el número de píxeles de filtrado supone un gran
coste. Una solución a este problema consiste en hacer una primera
interpolación entre pocos vecinos y ver si el resultado es constante
en todos ellos. Si todos los vecinos se encuentran en zona de luz no
es necesario estudiar más. De igual forma, si todos los vecinos se
encuentra en zona de sombra tampoco es necesario estudiar más. En el
caso en el que el pixel tenga vecinos en luz y en sombra, la
interpolación se realiza sobre un número de vecinos mayor.
La técnica Random Sampling está basada en esta idea. Además, para
generar los vecinos no se utiliza un patrón de desplazamientos
diagonales ya que estos provocan un degradado fijo que sigue
generando el efecto escalonado. En vez de esto se crean patrones de
desplazamiento aleatorios. Como los shaders no pueden generar
números aleatorios, este patrón se genera en la CPU y se almacena en
la GPU como una textura tridimensional. Cada texel de la textura
tridimensional almacena dos desplazamientos (uno en las coordenadas
XY y otro en las coordenadas ZW). Los desplazamientos se calculan de
forma que se distribuyan en forma de círculos concéntricos.

La clase CGModel debe modificarse para generar
la textura de desplazamientos aleatorios (método
BuildOffsetTex() ) y para asignar los valores de las variables
uniformes asociadas a esta textura en el método RenderScene().
void CGModel::initialize(int w, int h)
{
// Crea el programa gráfico para el entorno
skyboxProgram = new CGShaderProgram(IDR_SHADER1, IDR_SHADER2, -1, -1, -1);
if (skyboxProgram->IsLinked() == GL_FALSE) return;
// Crea el programa gráfico para la sombra
shadowProgram = new CGShaderProgram(IDR_SHADER5, IDR_SHADER6, -1, -1, -1);
if (shadowProgram->IsLinked() == GL_FALSE) return;
// Crea el programa gráfico para la escena
sceneProgram = new CGShaderProgram(IDR_SHADER3, IDR_SHADER4, -1, -1, -1);
if (sceneProgram->IsLinked() == GL_FALSE) return;
// Crea la cámara
camera = new CGCamera();
camera->SetPosition(0.0f, 5.0f, 30.0f);
// Crea el skybox
skybox = new CGSkybox();
// Crea la escena
scene = new CGScene();
// Crea el Framebuffer de la sombra
bool frameBufferStatus = InitShadowMap();
if (!frameBufferStatus) return;
// Inicializa la textura para antializasing
BuildOffsetTex(4, 4, 4);
// Asigna el viewport y el clipping volume
resize(w, h);
// Opciones de dibujo
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
}
//
// FUNCIÓN: CGModel::render()
//
// PROPÓSITO: Genera la imagen
//
void CGModel::render()
{
//*********************************************************//
// Genera el ShadowMap //
//*********************************************************//
// Activa el framebuffer de la sombra
glBindFramebuffer(GL_FRAMEBUFFER, shadowFBO);
// Activa el programa que genera el shadowmap
shadowProgram->Use();
// Asigna las matrices Viewport, View y Projection de la luz.
glm::mat4 lightViewMatrix = scene->GetLightViewMatrix();
glm::mat4 lightPerspective = glm::ortho(-150.0f, 150.0f,
-150.0f, 150.0f, 0.0f, 400.0f);
glm::mat4 lightMVP = lightPerspective * lightViewMatrix;
// Limpia la información de profundidad
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Activa front-face culling
glCullFace(GL_FRONT);
//Asigna el viewport
glViewport(0, 0, 1024, 1024);
// Dibuja la escena
scene->DrawShadow(shadowProgram, lightMVP);
//*********************************************************//
// Dibuja el skybox //
//*********************************************************//
// Activa el framebuffer de la imagen
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// Limpia el framebuffer
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Activa back-face culling
glCullFace(GL_BACK);
// Asigna el viewport
glViewport(0, 0, wndWidth, wndHeight);
// Dibuja el skybox
glm::mat4 view = camera->ViewMatrix();
skyboxProgram->Use();
skybox->Draw(skyboxProgram, projection, view);
//*********************************************************//
// Dibuja la escena //
//*********************************************************//
// Activa el programa de la escena
sceneProgram->Use();
sceneProgram->SetUniformI("ShadowMap", 1);
// Asigna las variables para anitializasing
glm::vec3 offsetTexSize = glm::vec3(4, 4, 8);
sceneProgram->SetUniformI("OffsetTex", 1);
sceneProgram->SetUniformVec3("OffsetTexSize", offsetTexSize);
sceneProgram->SetUniformF("Radius", 0.003);
// Dibuja la escena
glm::mat4 viewMatrix = camera->ViewMatrix();
scene->Draw(sceneProgram, projection, viewMatrix, lightMVP);
}
//
// FUNCIÓN: CGModel::BuildOffsetTex()
//
// PROPÓSITO: Inicializa la textura 3D con valores aleatorios
//
void CGModel::BuildOffsetTex(int texSize, int samplesU, int samplesV)
{
float TWOPI = glm::radians(360.0);
int size = texSize;
int samples = samplesU * samplesV;
int bufSize = size * size * samples * 2;
float *data = new float[bufSize];
for (int i = 0; i< size; i++)
{
for (int j = 0; j < size; j++)
{
for (int k = 0; k < samples; k += 2)
{
int x1, y1, x2, y2;
x1 = k % (samplesU);
y1 = (samples - 1 - k) / samplesU;
x2 = (k + 1) % samplesU;
y2 = (samples - 1 - k - 1) / samplesU;
glm::vec4 v;
// Center on grid and jitter
v.x = (x1 + 0.5f) + jitter();
v.y = (y1 + 0.5f) + jitter();
v.z = (x2 + 0.5f) + jitter();
v.w = (y2 + 0.5f) + jitter();
// Scale between 0 and 1
v.x /= samplesU;
v.y /= samplesV;
v.z /= samplesU;
v.w /= samplesV;
// Warp to disk
int cell = ((k / 2) * size * size + j * size + i) * 4;
data[cell + 0] = sqrtf(v.y) * cosf(TWOPI*v.x);
data[cell + 1] = sqrtf(v.y) * sinf(TWOPI*v.x);
data[cell + 2] = sqrtf(v.w) * cosf(TWOPI*v.z);
data[cell + 3] = sqrtf(v.w) * sinf(TWOPI*v.z);
}
}
}
glActiveTexture(GL_TEXTURE1);
glGenTextures(1, &offsetTexId);
glBindTexture(GL_TEXTURE_3D, offsetTexId);
glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA32F, size, size, samples/2, 0,
GL_RGBA, GL_FLOAT, data);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
delete[] data;
}
//
// FUNCIÓN: CGModel::jitter()
//
// PROPÓSITO: Genera un número aleatorio entre -0.5 y 0.5;
//
float CGModel::jitter()
{
return ((float)rand() / RAND_MAX) - 0.5f;
}
|
El VertexShader
en este caso no sufre modificaciones con respecto al utilizado en el
ShadowMap básico.
El FragmentShader contiene tres variables uniformes adicionales a
las utilizadas en la técnica ShadowMap básica. La variable OffsetTex
contiene la referencia a la textura 3D con los datos aleatorios. La
variable OffsetTexSize contiene el tamaño de la textura 3D.
La variable Radius contiene el factor de escala de los
desplazamientos aleatorios. Cada texel de la textura 3D contiene un
valor XYZW que almacena dos desplazamientos (XY y ZW). La función
shadeWithShadow() calcula en primer lugar el grado de
sombra de 8 puntos. Si el grado de sombra no es 1.0 (zona de luz) ni
0.0 (zona de sombra) sigue calculando el grado de sombra con el
resto de desplazamientos incluidos en la textura.
#version 400
in vec3 Position;
in vec3 Normal;
in vec2 TexCoord;
in vec4 ShadowCoord;
uniform sampler2D BaseTex;
uniform sampler2DShadow ShadowMap;
uniform mat4 ViewMatrix;
uniform sampler3D OffsetTex;
uniform vec3 OffsetTexSize; // (width, height, depth)
uniform float Radius;
struct LightInfo {
vec3 Ldir;
vec3 La;
vec3 Ld;
vec3 Ls;
};
uniform LightInfo Light;
struct MaterialInfo{
vec3 Ka;
vec3 Kd;
vec3 Ks;
float Shininess;
};
uniform MaterialInfo Material;
out vec4 FragColor;
vec3 ads(vec3 TexColor)
{
vec4 s4 = ViewMatrix*vec4(Light.Ldir, 0.0);
vec3 n = normalize(Normal);
vec3 v = normalize(-Position);
vec3 s = normalize(-vec3(s4));
vec3 r = reflect(-s, n);
float dRate = max(dot(s, n), 0.0);
float sRate = pow(max(dot(r, v), 0.0), Material.Shininess);
vec3 difusse = Light.Ld * Material.Kd * dRate;
vec3 specular = Light.Ls * Material.Ks * sRate;
return difusse*TexColor + specular;
}
void main()
{
vec3 TexColor = vec3( texture(BaseTex,TexCoord) );
vec3 ambient = Light.La * Material.Ka * TexColor;
vec3 diffAndSpec = ads(TexColor);
vec4 ShadowTexCoord = (ShadowCoord/ShadowCoord.w)*0.5 + vec4(0.5, 0.5, 0.5, 0.5);
ivec3 offsetCoord;
offsetCoord.xy = ivec2( mod( gl_FragCoord.xy, OffsetTexSize.xy ) );
float sum = 0.0;
int samplesDiv2 = int(OffsetTexSize.z);
vec4 sc = ShadowTexCoord;
for( int i = 0 ; i< 4; i++ )
{
offsetCoord.z = i;
vec4 offsets = texelFetch(OffsetTex,offsetCoord,0) *Radius * ShadowTexCoord.w;
sc.xy = ShadowTexCoord.xy + offsets.xy;
sum += textureProj(ShadowMap, sc);
sc.xy = ShadowTexCoord.xy + offsets.zw;
sum += textureProj(ShadowMap, sc);
}
float shadow = sum / 8.0;
if( shadow != 1.0 && shadow != 0.0 )
{
for( int i = 4; i< samplesDiv2; i++ )
{
offsetCoord.z = i;
vec4 offsets = texelFetch(OffsetTex, offsetCoord,0)*Radius*ShadowTexCoord.w;
sc.xy = ShadowTexCoord.xy + offsets.xy;
sum += textureProj(ShadowMap, sc);
sc.xy = ShadowTexCoord.xy + offsets.zw;
sum += textureProj(ShadowMap, sc);
}
shadow = sum / float(samplesDiv2 * 2.0);
}
FragColor = vec4(diffAndSpec * shadow + ambient, 1.0);
}
|
El resultado conseguido con esta técnica se puede apreciar en la siguiente captura.

|
|