Escuela Técnica Superior de Ingeniería

 

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.

 

 

Código de la práctica

 

 

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.

ShadowMapping

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.

Project11a

 

 

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.

Aliasing

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.

PCF

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.

Project11b

 

 

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.

RandomSampling

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.

Project11b