Escuela Técnica Superior de Ingeniería

 

Grado en Ingeniería Informática

Realidad Virtual

Curso 2023/2024

 

Práctica 9

Normal maps

 

Objetivos

 

En esta práctica se muestra como utilizar texturas para incorporar información asociada a los puntos internos de las primitivas. Esto permite incluir información sobre la dirección normal a cada punto que se utiliza para incorporar efectos de rugosidad en las imágenes.

 

 

Código de la práctica

 

 

Normal maps

 

Las texturas permiten proyectar una imagen sobre una primitiva, pero esa proyección es plana. Una forma de modelar el efecto de la rugosidad es considerar que las normales en cada punto de una primitiva no son iguales sino que cada punto tiene una normal diferente.

Normales

Para almacenar la información sobre la normal asociada a cada pixel se utiliza una textura, pero en este caso la información almacenada no se interpreta como un color RGB sino como las coordenadas del vector normal. Este tipo de texturas se conocen como Normal Maps. Para representar el vector normal es necesario fijar un sistema de coordenadas. Habitualmente se utiliza un sistema de coordenadas local a cada punto y tangente a la superficie. De esta forma, el eje X se considera paralelo a la coordenada U de la textura; el eje Y se considera paralelo al eje V de la textura; y el eje Z se considera perpendicular a la primitiva. Las coordenadas xyz del vector normal se almacenan como coordenadas rgb de la textura. La mayor parte de los puntos suele tener una dirección normal perpendicular a la primitiva (es decir, sobre el eje Z en el plano tangente) por lo que la interpretación de las texturas como colores suele dar un tono azul en la mayoría de los puntos.

Normales

Para poder interpretar la información de un normal map es necesario conocer no solo el vector normal a cada punto (que indica el eje Z), sino también los vectores tangente (eje X) y bitangente (eje Y). En realidad es suficiente con conocer el vector normal y el vector tangente, ya que el vector bitagente puede obtenerse como el producto vectorial de los anteriores.

Normales

Dados las posiciones de los tres vértices de un triángulo (en coordenadas locales) y las coordenadas de textura de dichos vértices se puede calcular los vectores tangente y bitangente. El vector E1 corresponde a la diferencia entre la posición de los vértices P2 y P1. El vector E2 corresponde a la diferencia entre la posición de los vértices P3 y P2. Los vectores E1 y E2 se pueden expresar en términos de los vectores tangente T y bitangente B.

Normales

A partir de esta ecuación se pueden calcular los valores de T y B. Esto permite calcular los valores de T y B para cada triángulo. En realidad necesitamos los valores de T y B para cada vértice. Esto se obtiene realizando una media entre los valores de los triángulos a los que pertenece cada vértice.

Normales

 

 

El programa gráfico

 

Para incorporar la técnica de NormalMapping es necesario incluir un nuevo atributo asociado a los vértices, que corresponde al vector tangente expresado en coordenadas locales. El modelo de iluminación de Phong se basa en tres vectores: el vector normal, el vector de posición desde el observador y la dirección de la luz. El problema es que cada uno de estos vectores está descrito en un sistema de coordenadas diferente. La dirección de la luz se define en coordenadas del modelo. La posición del punto desde el observador se obtiene proyectando el atributo VertexPosition y se define en coordenadas de observador. La normal se leerá del fichero de textura NormalMap y está descrita en coordenadas del espacio tangente.

A continuación se muestra el contenido del VertexShader asociado a la técnica de NormalMapping. Las salidas del VertexShader se van a expresar en el sistema de coordenadas tangente. Para ello es necesario transformar las coordenadas de observador (que pueden obtenerse con las matrices de transformación ModelViewMatrix y ViewMatrix) en coordenadas tangentes. Dentro del shader se calcula la matriz de transformación TBN utilizando los vectores VertexNormal y VertexTangent. A continuación se calcula las variables de salida LightDir y ViewDir en coordenadas tangentes.

#version 400

layout (location = 0) in vec3 VertexPosition;
layout (location = 1) in vec3 VertexNormal;
layout (location = 2) in vec2 VertexTexCoord;
layout (location = 3) in vec3 VertexTangent;

struct LightInfo {
  vec3 Ldir;
  vec3 La;
  vec3 Ld;
  vec3 Ls;
};
uniform LightInfo Light;

uniform mat4 ModelViewMatrix;
uniform mat4 ViewMatrix;
uniform mat4 MVP;

out vec3 LightDir;
out vec2 TexCoord;
out vec3 ViewDir;

void main()
{
  vec4 n4 = ModelViewMatrix*vec4(VertexNormal,0.0);
  vec4 t4 = ModelViewMatrix*vec4(VertexTangent,0.0);
  vec4 v4 = ModelViewMatrix*vec4(VertexPosition,1.0);

  // Vectores unitarios del espacio tangente
  vec3 N = normalize( vec3(n4) );
  vec3 T = normalize( vec3(t4) );
  vec3 B = normalize( cross(N,T) );

  // Matriz de transformación al espacio tangente
  mat3 TBN = mat3(
                  T.x, B.x, N.x,
                  T.y, B.y, N.y,
                  T.z, B.z, N.z);

  LightDir = vec3( ViewMatrix * vec4(Light.Ldir,0.0));
  LightDir = normalize(TBN * LightDir);
  ViewDir = TBN * normalize( vec3( -v4 ) );
  TexCoord = VertexTexCoord;
  gl_Position = MVP * vec4( VertexPosition, 1.0);
}

El FragmentShader recibe como entrada las propiedades de la luz y el material, así como las texturas necesarias y los vectores de dirección de la luz y del punto en coordenadas del espacio tangente. La función ads() se ha modificado para recibir como parámetros el vector normal y los colores de la textura ambiental-difusa y de la textura especular. El método main() se encarga de leer la información de las texturas y ejecutar la función ads() para obtener el color final asociado a cada pixel del fragmento.

#version 400

in vec3 LightDir;
in vec2 TexCoord;
in vec3 ViewDir;

uniform bool NormalMapping;

uniform sampler2D ColorTex;
uniform sampler2D NormalMapTex;
uniform sampler2D SpecMapTex;

struct LightInfo {
  vec3 Ldir;
  vec3 La;
  vec3 Ld;
  vec3 Ls;
};
uniform LightInfo Light;

struct MaterialInfo{
  vec3 Ka;
  vec3 Kd;
  vec3 Ks;
  float Shininess;
  float Dissolved;
};
uniform MaterialInfo Material;

out vec4 FragColor;

vec3 ads(vec3 norm, vec3 tex, vec3 spec) {
  vec3 r = reflect(LightDir,norm);
  float dRate = max(dot(-LightDir,norm), 0.0);
  float sRate = pow(max(dot(r,ViewDir),0.0),Material.Shininess);

  vec3 ambient = Light.La * Material.Ka;
  vec3 difusse = Light.Ld * Material.Kd * dRate;
  vec3 specular = Light.Ls * Material.Ks * sRate;
  return (ambient + difusse)*tex + specular*spec;
}

void main() {
  vec4 normal = vec4(0.0,0.0,1.0,1.0);
  if(NormalMapping) normal = 2.0*texture(NormalMapTex,TexCoord)-1.0;
  vec4 texColor = texture( ColorTex, TexCoord );
  vec4 specColor = texture(SpecMapTex, TexCoord);
  FragColor = vec4(ads(normal.xyz, texColor.rgb, specColor.rgb),Material.Dissolved);
}

 

 

La clase CGMaterial

 

En esta práctica hemos incluido algunas modificaciones a la definición de los materiales. Por un lado, es necesario incluir la textura asociada al NormalMap. Además, se ha incluido la textura asociada a la la luz especular y el campo Dissolved para almacenar el valor de la opacidad del material. El contenido del archivo de cabecera de la clase CGMaterial es el siguiente:

#pragma once

#include <glm/glm.hpp>
#include "CGShaderProgram.h"

class CGMaterial {

private:
  glm::vec3 Ka;       // Reflectividad ambiental (color ante la luz ambiental)
  glm::vec3 Kd;       // Reflectividad difusa (color ante la luz difusa)
  glm::vec3 Ks;       // Reflectividad especular (color ante la luz especular)
  GLfloat Shininess;  // Factor de brillo (comportamiento ante la luz especular)
  GLfloat Dissolved;  // Opacidad (1.0 opaco, 0.0 transparente)
  GLuint textureId;   // Identificador de la textura básica
  GLuint normalmapId; // Identificador de la textura de normales
  GLuint specmapId;   // Identificador de la textura de luz especular

public:
  CGMaterial();
  void SetAmbientReflect(GLfloat r, GLfloat g, GLfloat b);
  void SetDifusseReflect(GLfloat r, GLfloat g, GLfloat b);
  void SetSpecularReflect(GLfloat r, GLfloat g, GLfloat b);
  void SetShininess(GLfloat f);
  void SetDissolved(GLfloat d);

  void SetTexture(GLuint id);
  void SetNormalMap(GLuint id);
  void SetSpecMap(GLuint id);
  GLuint LoadTexture(char* filename);
  GLuint LoadTexture(int idr);
  void SetUniforms(ShaderProgram* program);
  GLuint GetTexture();
  GLuint GetNormalMap();
  GLuint GetSpecMap();
};

Los nuevos campos tienen asociados sus respectivos métodos Get() y Set(). Para no repetir los métodos de inicialización de ficheros de textura, se han sustituido los métodos InitTexture() por los métodos LoadTexture() que cargan la textura desde un fichero externo o desde un recurso y crean un identificador que puede asociarse tanto a la textura principal como a la textura especular y al normal map. También es necesario modificar el constructor para introducir los valores por defecto de los nuevos campos y el método SetUniforms() para asignar las propiedades del material al programa gráfico.

#include "CGMaterial.h"
#include <GL/glew.h>
#include <FreeImage.h>

//
// FUNCIÓN: CGMaterial::CGMaterial()
//
// PROPÓSITO: Construye un material con los valores por defecto
//
CGMaterial::CGMaterial()
{
  Ka = glm::vec3(1.0f, 1.0f, 1.0f);
  Kd = glm::vec3(1.0f, 1.0f, 1.0f);
  Ks = glm::vec3(0.8f, 0.8f, 0.8f);
  Shininess = 16.0f;
  Dissolved = 1.0f;
  textureId = 0;
  normalmapId = 0;
  specmapId = 0;
}

//
// FUNCIÓN: GLuint CGMaterial::LoadTexture(const char* filename)
//
// PROPÓSITO: Carga una textura
//
GLuint CGMaterial::LoadTexture(const char* filename)
{
  GLuint texId;
  FREE_IMAGE_FORMAT format = FreeImage_GetFileType(filename, 0);
  FIBITMAP* bitmap = FreeImage_Load(format, filename);
  FIBITMAP *pImage = FreeImage_ConvertTo32Bits(bitmap);
  int nWidth = FreeImage_GetWidth(pImage);
  int nHeight = FreeImage_GetHeight(pImage);

  glGenTextures(1, &texId);
  glBindTexture(GL_TEXTURE_2D, texId);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, nWidth, nHeight,
               0, GL_BGRA, GL_UNSIGNED_BYTE, (void*)FreeImage_GetBits(pImage));

  FreeImage_Unload(pImage);

  return texId;
}

//
// FUNCIÓN: GLuint CGMaterial::LoadTexture(int idr)
//
// PROPÓSITO: Carga una textura a partir de un recurso
//
GLuint CGMaterial::LoadTexture(int idr)
{
  GLuint texId;

  HRSRC handle = FindResource(NULL, MAKEINTRESOURCE(idr), L"IMAGE");
  HGLOBAL hGlobal = LoadResource(NULL, handle);
  LPCTSTR rsc_ptr = static_cast<LPCTSTR>(LockResource(hGlobal));
  DWORD mem_size = SizeofResource(NULL, handle);
  BYTE* mem_buffer = (BYTE*)malloc((mem_size) * sizeof(BYTE));
  memcpy(mem_buffer, rsc_ptr, mem_size * sizeof(BYTE));
  FreeResource(hGlobal);

  FIMEMORY* hmem = FreeImage_OpenMemory(mem_buffer, mem_size * sizeof(BYTE));
  FREE_IMAGE_FORMAT fif = FreeImage_GetFileTypeFromMemory(hmem, 0);
  FIBITMAP* check = FreeImage_LoadFromMemory(fif, hmem, 0);
  FIBITMAP* pImage = FreeImage_ConvertTo32Bits(check);
  int nWidth = FreeImage_GetWidth(pImage);
  int nHeight = FreeImage_GetHeight(pImage);

  glGenTextures(1, &texId);
  glBindTexture(GL_TEXTURE_2D, texId);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, nWidth, nHeight,
               0, GL_BGRA, GL_UNSIGNED_BYTE, (void*)FreeImage_GetBits(pImage));

  FreeImage_Unload(pImage);
  FreeImage_CloseMemory(hmem);
  free(mem_buffer);

  return texId;
}

//
// FUNCIÓN: CGMaterial::SetUniforms(CGShaderProgram* program)
//
// PROPÓSITO: Configura las propiedades de material en el programa gráfico
//
void CGMaterial::SetUniforms(CGShaderProgram* program)
{
  program->SetUniformVec3("Material.Ka", Ka);
  program->SetUniformVec3("Material.Kd", Kd);
  program->SetUniformVec3("Material.Ks", Ks);
  program->SetUniformF("Material.Shininess", Shininess);
  program->SetUniformF("Material.Dissolved", Dissolved);
  program->SetUniformI("BaseTex", 0);
  program->SetUniformI("NormalMapTex", 1);
  program->SetUniformI("SpecMapTex", 2);
  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, textureId);
  glActiveTexture(GL_TEXTURE1);
  glBindTexture(GL_TEXTURE_2D, normalmapId);
  glActiveTexture(GL_TEXTURE2);
  glBindTexture(GL_TEXTURE_2D, specmapId);
}

...

 

 

La clase CGPiece

 

Para poder desarrollar la técnica de NormalMapping es necesario incluir un nuevo atributo asociado a cada vértice con la información del vector tangente en coordenadas locales. Esto supone modificar la clase CGPiece para incluir el nuevo atributo. Las subclases de CGPiece generan los valores de los atributos VertexPosition, VertexNormal y VertexTexCoord. Los valores del nuevo atributo (VertexTangent) deben calcularse analizando las primitivas que forman la pieza para calcular el vector tangente en función de las posiciones y coordenadas de textura, tal y como se explicó al principio. El fichero de cabecera de la clase CGPiece queda así:

#pragma once

#include <GL/glew.h>
#include <glm/glm.hpp>
#include "CGMaterial.h"
#include "CGShaderProgram.h"

#define VERTEX_DATA   0
#define INDEX_DATA    1
#define NORMAL_DATA   2
#define TEXTURE_DATA  3
#define TANGENT_DATA  4

//
// CLASE: CGPiece
//
// DESCRIPCIÓN: Clase abstracta que representa una pieza descrita mediante
//              VAO para su renderizado mediante shaders
// 
class CGPiece {
protected:
  GLushort* indexes; // Array of indexes 
  GLfloat* vertices; // Array of vertices
  GLfloat* normals;  // Array of normals
  GLfloat* textures; // Array of texture coordinates
  GLfloat* tangents; // Array of tangent vectors

  GLuint numFaces; // Number of faces
  GLuint numVertices; // Number of vertices
  GLuint VBO[5];
  GLuint VAO;

  glm::mat4 location; // Model matrix
  CGMaterial* material;

public:
  ~CGPiece();
  void InitTangents();
  void InitBuffers();
  void SetMaterial(CGMaterial* m);
  void SetLocation(glm::mat4 loc);
  glm::mat4 GetLocation();
  void Translate(glm::vec3 t);
  void Rotate(GLfloat angle, glm::vec3 axis);
  void Draw(CGShaderProgram* prog, glm::mat4 proj, glm::mat4 view, glm::mat4 model);
};

El código de la clase incluye un nuevo método llamado InitTangents() dedicado a calcular los vectores tangentes. Además, el método InitBuffers() debe modificarse para incluir el nuevo atributo asociado a los vértices. Las modificaciones al fichero de código de la clase CGPiece son las siguientes:

#include "CGPiece.h"
#include <GL/glew.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include "CGMaterial.h"

//
// FUNCIÓN: CGPiece::~CGPiece()
//
// PROPÓSITO: Destructor de la pieza
//
CGPiece::~CGPiece()
{
  if (vertices != NULL) delete[] vertices;
  if (indexes != NULL) delete[] indexes;
  if (normals != NULL) delete[] normals;
  if (textures != NULL) delete[] textures;
  if (tangents != NULL) delete[] tangents;

  // Delete vertex buffer objects
  glDeleteBuffers(5, VBO);
  glDeleteVertexArrays(1, &VAO);
}

//
// FUNCIÓN: CGPiece::InitTangents()
//
// PROPÓSITO: Calcula los vectores tangente para realizar normal mapping
//
void CGPiece::InitTangents()
{
  int* tangentCount = new int[numVertices];
  for (int i = 0; i < numVertices; i++) tangentCount[i] = 0;

  tangents = new GLfloat[3*numVertices];
  for (int i = 0; i < 3*numVertices; i++) tangents[i] = 0.0;

  for (int i = 0; i < numFaces; i++)
  {
    int a = indexes[3 * i];
    int b = indexes[3 * i + 1];
    int c = indexes[3 * i + 2];

    GLfloat Ax = vertices[3 * a];
    GLfloat Ay = vertices[3 * a + 1];
    GLfloat Az = vertices[3 * a + 2];
    GLfloat Bx = vertices[3 * b];
    GLfloat By = vertices[3 * b + 1];
    GLfloat Bz = vertices[3 * b + 2];
    GLfloat Cx = vertices[3 * c];
    GLfloat Cy = vertices[3 * c + 1];
    GLfloat Cz = vertices[3 * c + 2];

    GLfloat Au = textures[2 * a];
    GLfloat Av = textures[2 * a + 1];
    GLfloat Bu = textures[2 * b];
    GLfloat Bv = textures[2 * b + 1];
    GLfloat Cu = textures[2 * c];
    GLfloat Cv = textures[2 * c + 1];

    GLfloat E1x = Bx - Ax;
    GLfloat E1y = By - Ay;
    GLfloat E1z = Bz - Az;
    GLfloat E2x = Cx - Bx;
    GLfloat E2y = Cy - By;
    GLfloat E2z = Cz - Bz;

    GLfloat U1 = Bu - Au;
    GLfloat V1 = Bv - Av;
    GLfloat U2 = Cu - Bu;
    GLfloat V2 = Cv - Bv;

    GLfloat f = U1*V2 - U2*V1;
    GLfloat Tx = (V2*E1x - V1*E2x) / f;
    GLfloat Ty = (V2*E1y - V1*E2y) / f;
    GLfloat Tz = (V2*E1z - V1*E2z) / f;

    tangents[3*a] = (tangents[3*a]*tangentCount[a] + Tx)/(tangentCount[a]+1);
    tangents[3*a+1] = (tangents[3*a+1]*tangentCount[a] + Ty) / (tangentCount[a]+1);
    tangents[3*a+2] = (tangents[3*a+2]*tangentCount[a] + Tz) / (tangentCount[a]+1);
    tangentCount[a]++;

    tangents[3*b] = (tangents[3*b]*tangentCount[b] + Tx) / (tangentCount[b]+ 1);
    tangents[3*b+1] = (tangents[3*b+1]*tangentCount[b] + Ty) / (tangentCount[b]+1);
    tangents[3*b+2] = (tangents[3*b+2]*tangentCount[b] + Tz) / (tangentCount[b]+1);
    tangentCount[b]++;

    tangents[3*c] = (tangents[3*c]*tangentCount[c] + Tx) / (tangentCount[c]+1);
    tangents[3*c+1] = (tangents[3*c+1]*tangentCount[c] + Ty) / (tangentCount[c]+1);
    tangents[3*c+2] = (tangents[3*c+2]*tangentCount[c] + Tz) / (tangentCount[c]+1);
    tangentCount[c]++;
  }

  delete[] tangentCount;
}

//
// FUNCIÓN: CGPiece::InitBuffers()
//
// PROPÓSITO: Crea el VAO y los VBO y almacena todos los datos
//            en la GPU.
//
void CGPiece::InitBuffers()
{
  InitTangents();

  // Create the Vertex Array Object
  glGenVertexArrays(1, &VAO);
  glBindVertexArray(VAO);

  // Create the Vertex Buffer Objects
  glGenBuffers(5, VBO);

  int bufferSize = sizeof(GLfloat)*numVertices * 3;
  
  // Copy data to video memory
  // Vertex data
  glBindBuffer(GL_ARRAY_BUFFER, VBO[VERTEX_DATA]);
  glBufferData(GL_ARRAY_BUFFER, bufferSize, vertices, GL_STATIC_DRAW);

  // Normal data
  glBindBuffer(GL_ARRAY_BUFFER, VBO[NORMAL_DATA]);
  glBufferData(GL_ARRAY_BUFFER, bufferSize, normals, GL_STATIC_DRAW);

  // Tangent data
  glBindBuffer(GL_ARRAY_BUFFER, VBO[TANGENT_DATA]);
  glBufferData(GL_ARRAY_BUFFER, bufferSize, tangents, GL_STATIC_DRAW);

  // Texture coordinates
  bufferSize = sizeof(GLfloat)*numVertices * 2;
  glBindBuffer(GL_ARRAY_BUFFER, VBO[TEXTURE_DATA]);
  glBufferData(GL_ARRAY_BUFFER, bufferSize, textures, GL_STATIC_DRAW);


  // Indexes
  bufferSize = sizeof(GLushort)*numFaces * 3;
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, VBO[INDEX_DATA]);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, bufferSize, indexes, GL_STATIC_DRAW);

  delete[] vertices;
  delete[] indexes;
  delete[] normals;
  delete[] textures;
  delete[] tangents;

  vertices = NULL;
  indexes = NULL;
  normals = NULL;
  textures = NULL;
  tangents = NULL;

  glEnableVertexAttribArray(0); // Vertex position
  glBindBuffer(GL_ARRAY_BUFFER, VBO[VERTEX_DATA]);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);

  glEnableVertexAttribArray(1); // Vertex normals
  glBindBuffer(GL_ARRAY_BUFFER, VBO[NORMAL_DATA]);
  glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, 0);

  glEnableVertexAttribArray(2); // Vertex textures
  glBindBuffer(GL_ARRAY_BUFFER, VBO[TEXTURE_DATA]);
  glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 0, 0);

  glEnableVertexAttribArray(3); // Vertex tangents
  glBindBuffer(GL_ARRAY_BUFFER, VBO[TANGENT_DATA]);
  glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, 0, 0);
}

...

 

 

La clase CGScene

 

La escena a mostrar en este caso está formada por dos figuras importadas de ficheros esternos. Se trata de las figuras Bridge (un puente de piedra) y Muro (un trabajador). La clase CGScene contiene las dos figuras (figure0 y figure1) y un campo (figure) para seleccionar cual de las dos figuras se muestra en cada momento. La clase contiene además otro campo (normalMapping) que permite activar o desactivar la técnica de normal mapping. El fichero de cabecera de la clase CGScene queda así:

#pragma once

#include <GL/glew.h>
#include <glm/glm.hpp>
#include "CGShaderProgram.h"
#include "CGLight.h"
#include "CGObject.h"

class CGScene {
public:
  CGScene();
  ~CGScene();
  void Draw(CGShaderProgram* program, glm::mat4 proj, glm::mat4 view);
  void ChangeFigure();
  void ChangeMapping();
private:
  CGObject* figure0;
  CGObject* figure1;
  CGLight* light;
  int figure;
  GLboolean normalMapping;
};

El código de la clase incluye dos nuevos métodos llamados ChangeFigure(), dedicado cambiar la figura a mostrar, y el método ChangeMapping(), dedicado a activar o desactivar la opción de normal mapping. El fichero de código de la clase CGScene es el siguiente:

#include "CGScene.h"
#include <GL/glew.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include "CGShaderProgram.h"
#include "CGObject.h"
#include "CGLight.h"
#include "Bridge.h"
#include "Muro.h"

//
// FUNCIÓN: CGScene::CGScene()
//
// PROPÓSITO: Construye la escena
//
CGScene::CGScene()
{
  glm::vec3 Ldir = glm::vec3(1.0f, -0.8f, -1.0f);
  Ldir = glm::normalize(Ldir);
  light = new CGLight();
  light->SetLightDirection(Ldir);
  light->SetAmbientLight(glm::vec3(0.2f, 0.2f, 0.2f));
  light->SetDifusseLight(glm::vec3(0.8f, 0.8f, 0.8f));
  light->SetSpecularLight(glm::vec3(1.0f, 1.0f, 1.0f));
  
  figure0 = new Bridge();
  figure1 = new Muro();
  figure = 0;
  normalMapping = GL_TRUE;
}

//
// FUNCIÓN: CGScene::~CGScene()
//
// PROPÓSITO: Destruye el objeto que representa la escena
//
CGScene::~CGScene()
{
  delete figure0;
  delete figure1;
  delete light;
}

//
// FUNCIÓN: CGScene::Draw()
//
// PROPÓSITO: Dibuja la escena
//
void CGScene::Draw(CGShaderProgram* program, glm::mat4 proj, glm::mat4 view)
{
  light->SetUniforms(program);
  if(normalMapping == GL_TRUE) program->SetUniformI("NormalMapping", 1);
  else program->SetUniformI("NormalMapping", 0);

  switch (figure)
  {
    case 0: figure0->Draw(program, proj, view); break;
    case 1: figure1->Draw(program, proj, view); break;
  }
}

//
// FUNCIÓN: CGScene::ChangeFigure()
//
// PROPÓSITO: Cambia la figura a mostrar
//
void CGScene::ChangeFigure()
{
  figure = (figure + 1) % 2;
}

//
// FUNCIÓN: CGScene::ChangeMapping()
//
// PROPÓSITO: Activa o desactiva el normal mapping
//
void CGScene::ChangeMapping()
{
  normalMapping = !normalMapping;
}

 

 

Resultado final

 

Para terminar se ha modificado el método KeyboardAction() de la clase CGModel para que la aplicación cambie de figura (utilizando la tecla 'F') o de forma de mapeo de la normal (utilizando la tecla 'M'). A continuación se muestran capturas de estas figuras activando o desactivando la opción de normal mapping.

Bridge off

Bridge on

Muro off

Muro on