Escuela Técnica Superior de Ingeniería

 

Grado en Ingeniería Informática

Realidad Virtual

Curso 2023/2024

 

Práctica 7

Cubemaps

 

Objetivos

 

El objetivo de esta práctica es describir la forma de definir una textura de fondo que muestre el entorno en 360º. Para ello se utiliza un tipo de textura especial denominado CubeMap. El objeto utilizado para desarrollar este entorno se conoce como "SkyBox". El resultado de añadir esta textura de fondo se observa en la siguiente captura.

Scene

 

 

Código de la práctica

 

 

La textura CubeMap

 

La textura Cubemap es un tipo de textura diseñada para ocupar todo el espacio visual. Está formada por 6 imágenes en 2D que corresponden a las proyecciones sobre un cubo. Las imágenes deben estar diseñadas para ofrecer una imagen continua entre las caras del cubo siempre que se observen desde el centro del cubo.

Scene Scene

La aplicación de las texturas de tipo Cubemap utiliza tres coordenadas espaciales que corresponden a la posición del punto sobre el cubo en el sistema de coordenadas del centro del cubo. A partir de las tres coordenadas (rx,ry,rz) se calcula a cual de las seis imágenes pertenece el punto y las coordenadas de textura 2D correspondientes en dicha imagen.

Coordenadas

La ecuación anterior provoca un efecto curioso. Supongamos que queremos calcular el texel asociado a las coordenadas (0,0.5,-1). Este punto se encuentra en la cara -Z en una posición centrada en el eje X y por encima del centro en el eje Y. Al aplicar la ecuación se obtienen las coordenadas de textura (0.5, 0.25), es decir, un punto de la imagen centrado en el eje X pero por debajo del centro en el eje Y. Dicho de otra manera, la ecuación anterior provoca que las imágenes de los ejes +X, -X, +Z y -Z se muestren invertidas. Por esa razón debemos invertir verticalmente las imágenes utilizadas en estos ejes. 

 

 

La clase CGSkybox

 

Para dibujar el Skybox necesitamos definir una figura que describa un telón de fondo. Para ello solo se necesitan cuatro vértices para generar un rectángulo. La figura contendrá también la referencia a la textura cubemap y métodos para cargar las imágenes. El comportamiento de esta figura es diferente de los objetos que incluimos en los modelos, así que necesitamos definir una nueva clase que no es subclase de CGFigure. A continuación se muestra el fichero de cabecera que describe la estructura de la clase CGSkybox.

#pragma once

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

class CGSkybox {
public:
  CGSkybox();
  ~CGSkybox();
  void Draw(CGShaderProgram * program, glm::mat4 projection, glm::mat4 view);

private:
  GLuint cubemap;
  GLuint VBO[2];
  GLuint VAO;

  void InitCube();
  void InitCubemap();
  void InitTexture(GLuint target, const char* filename);
  void InitTexture(GLuint target, int idr);
};

El constructor de la clase realiza dos acciones: cargar la textura cubemap y crear el VAO que describe el telón de fondo. El método InitCubemap() se encarga de crear la textura y cargar las seis imágenes que la forman. Para cargar las imágenes se utiliza el método InitTexture() con un contenido similar al que utiliza la clase CGMaterial. Se han incluido dos versiones de este método para poder cargar las imágenes desde fihceros externos o desde recursos de la aplicación. .El método InitCube() crea el Vertex Array Object y los buffers que describen el rectángulo utilizado como telón de fondo. Las posiciones de los vértices están descritas directamente en coordenadas Clip. La forma en que se transforman los atributos del Skybox y se pintan los píxeles es diferente de la utilizada en las figuras normales, por lo que debemos desarrollar una versión diferente del VertexShader y del FragmentShader.

El método Draw() lanza el proceso de dibujo del telón de fondo y requiere asignar las variables específicas del programa gráfico, incluyendo la asignación de la textura cubemap como textura activa 0. Como veremos a continuación, el VertexShader en este caso utiliza una matriz llamada Inverse que corresponde a la transformación entre el sistema de coordenadas Clip y el sistema de coordenadas del modelo. Para obtener esta transformación se calcula primero la transformación entre el modelo y el observador (solo la orientación) y entre el observador y el volumen clip (matriz projection). La transformación que necesitamos (de clip a modelo) es la inversa de ésta (de modelo a clip). El método glDepthMask() se utiliza para activar y desactivar la escritura del buffer de profundidad.

#include "CGSkybox.h"
#include <GL/glew.h>
#include <FreeImage.h>
#include "CGFigure.h"

//
// FUNCIÓN: CGSkybox::CGSkybox()
//
// PROPÓSITO: Construye el objeto que describe la imagen de fondo
//
CGSkybox::CGSkybox()
{
  InitCubemap();
  InitCube();
}

//
// FUNCIÓN: CGSkybox::~CGSkybox()
//
// PROPÓSITO: Destruye el objeto que describe la imagen de fondo
//
CGSkybox::~CGSkybox()
{
  glDeleteBuffers(2, VBO);
  glDeleteVertexArrays(1, &VAO);
  glDeleteTextures(1, &cubemap);
}

//
// FUNCIÓN: CGSkybox::InitCube()
//
// PROPÓSITO: Inicialliza los buffers con los vértices del telón
//
void CGSkybox::InitCube()
{
  GLfloat vertices[12] = {
    -1.0f, -1.0f, -1.0f,
    1.0f, -1.0f, -1.0f,
    1.0f, 1.0f, -1.0f,
    -1.0f, 1.0f, -1.0f
  };
  
  GLushort indexes[6] = {
    0,1,2,
    0,2,3 
  };

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

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

  // Copy data to video memory
  // Vertex data
  glBindBuffer(GL_ARRAY_BUFFER, VBO[VERTEX_DATA]);
  glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat)*12, vertices, GL_STATIC_DRAW);

  // Indexes
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, VBO[INDEX_DATA]);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLushort)*6, indexes, GL_STATIC_DRAW);

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

//
// FUNCIÓN: CGSkybox::InitCubemap()
//
// PROPÓSITO: Inicialliza las texturas del cubo
//
void CGSkybox::InitCubemap()
{
  glActiveTexture(GL_TEXTURE1);

  glGenTextures(1, &cubemap);
  glBindTexture(GL_TEXTURE_CUBE_MAP, cubemap);

  // Versión con recursos
  InitTexture(GL_TEXTURE_CUBE_MAP_POSITIVE_X, IDR_IMAGE4);
  InitTexture(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, IDR_IMAGE5);
  InitTexture(GL_TEXTURE_CUBE_MAP_POSITIVE_Y, IDR_IMAGE6);
  InitTexture(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, IDR_IMAGE7);
  InitTexture(GL_TEXTURE_CUBE_MAP_POSITIVE_Z, IDR_IMAGE8);
  InitTexture(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, IDR_IMAGE9);

  // Versión con ficheros
  // InitTexture(GL_TEXTURE_CUBE_MAP_POSITIVE_X, "textures/posx.jpg");
  // InitTexture(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, "textures/negx.jpg");
  // InitTexture(GL_TEXTURE_CUBE_MAP_POSITIVE_Y, "textures/posy.jpg");
  // InitTexture(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, "textures/negy.jpg");
  // InitTexture(GL_TEXTURE_CUBE_MAP_POSITIVE_Z, "textures/posz.jpg");
  // InitTexture(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, "textures/negz.jpg");
	
  // Typical cube map settings
  glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER,	GL_LINEAR);
  glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER,	GL_LINEAR);
  glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S,	GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T,	GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R,	GL_CLAMP_TO_EDGE);
}

//
// FUNCIÓN: CGSkybox::InitTexture(GLuint target, const char* filename)
//
// PROPÓSITO: Carga una textura
//
void CGSkybox::InitTexture(GLuint target, const char* filename)
{
  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);

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

  FreeImage_Unload(pImage);
}

//
// FUNCIÓN: void CGSkybox::InitTexture(GLuint target, int idr)
//
// PROPÓSITO: Carga una textura a partir de un recurso
//
void CGSkybox::InitTexture(GLuint target, int idr)
{
  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);

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

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

//
// FUNCIÓN: CGSkybox::Draw()
//
// PROPÓSITO: Dibuja la imagen de fondo
//
void Skybox::Draw(CGShaderProgram * program, glm::mat4 projection, glm::mat4 view)
{
  glm::mat3 rot3 = glm::mat3(view); // Parte rotacional de la matriz View
  glm::mat4 rot4 = glm::mat4(rot3);
  glm::mat4 mvp = projection * rot4; // Transformación del Skybox a coordenadas Clip
  glm::mat4 inv = glm::inverse(mvp); // Transformación de coordenadas Clip a coordenadas de modelo del Skybox

  program->SetUniformMatrix4("Inverse", inv);
  program->SetUniformI("CubemapTex", 0);

  glDepthMask(GL_FALSE);
  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_CUBE_MAP, cubemap);

  glBindVertexArray(VAO);
  glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL);
  glDepthMask(GL_TRUE);
}

 

 

El programa gráfico

 

El dibujo del Skybox requiere un tratamiento diferente del que utilizamos para el resto de objetos que forman la escena, por lo que utilizaremos un programa gráfico específico para dibujarlo.

Con respecto al código del VertexShader, el Skybox va a recibir como atributo VertexPosition la posición de los vértices en coordenadas Clip así que la variable de salida gl_Position se copia directamente del atributo. Las coordenadas de textura para el cubemap requieren tres componentes (rx,ry,rz) y se van a almacenar en la variable de salida Position. Estas coordenadas se calculan como las posiciones del telón de fondo expresadas en coordenadas del modelo. Esto requiere una transformación entre el sistema de coordenadas Clip y el sistema de coordenadas del modelo que debe estar recogida en la matriz Inverse.

 #version 400

layout(location = 0) in vec3 VertexPosition;

uniform mat4 Inverse;

out vec3 Position;

void main()
{
  gl_Position = vec4(VertexPosition,1.0);
  Position = vec3(Inverse * gl_Position);
  Position = normalize(Position);
}

Por su parte, el código del FragmentShader se limita a buscar el color de la textura cubemap asociado a las coordenadas del telón de fondo. La textura cubemap se introduce como una variable uniforme (CubemapTex) de tipo samplerCube. Las coordenadas del telón de fondo corresponden a la variable de entrada Position generada en el VertexShader.

#version 400

in vec3 Position;

out vec4 FragColor;

uniform samplerCube CubemapTex;

void main()
{
  FragColor = texture(CubemapTex,Position);
}

 

 

Los recursos de la aplicación

 

Para introducir el Skybox se han añadido nuevos recursos a la aplicación. Por una parte es necesario incluir las 6 imágenes correspondientes al cubemap. Para ello se han definido los recursos IDR_IMAGE4 a IDR_IMAGE9. Por otro lado es necesario incluir los shaders utilizados en el programa gráfico dedicado a mostrar el Skybox. Estos shaders se han incluido como los recursos IDR_SHADER3 e IDR_SHADER4.

Recursos

Los recursos correspondientes a las texturas se toman desde los ficheros de imágenes incluidos en el directorio "textures". Si no se desea utilizar recursos se pueden cargar las texturas directamente a partir de estos ficheros. Las funciones que cargan las texturas desde ficheros externos se han incluido comentadas en el código fuente de la aplicación. Como puede observarse en la siguiente captura, las imágenes de los ejes X y Z del cubemap han sido volteadas verticalmente. El código de la práctica incluye una directorio llamado Textures con numerosos ejemplos de texturas que pueden utiulizarse como cubemaps.

Texturas

Los recursos correspondientes a las shaders se toman desde los ficheros GLSL incluidos en el directorio "shaders". Estos ficheros se pueden cargar directamente si no se desea utilizar recursos. Los shaders correspondientes al programa gráfico asociado al Skybox se definen en los ficheros SkyboxVertexShader.glsl y SkyboxFragmentShader.glsl

Shaders

 

 

La clase CGModel

 

La clase CGModel se ha modificado para incluir el objeto skybox y el programa gráfico vinculado a él. En este caso se ha incluido además un método CameraCosntraints() para impedir que la cámara se pueda alejar demasiado de los objetos de la escena.

#pragma once

#include <GL/glew.h>
#include "CGShaderProgram.h"
#include "CGScene.h"
#include "CGSkybox.h"
#include "CGCamera.h"

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;

  void CameraConstraints();
};

El fichero de código es el siguiente.

#include "CGModel.h"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <GLFW/glfw3.h>
#include <iostream>
#include "CGCamera.h"
#include "CGScene.h"
#include "CGSkybox.h"
#include "resource.h"

//
// 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("shaders/SkyboxVertexShader.glsl",
  //                                     "shaders/SkyboxFragmentShader.glsl", NULL, NULL, NULL);
  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();

  // 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);
}

//
// FUNCIÓN: CGModel::finalize()
//
// PROPÓSITO: Libera los recursos del modelo 3D
//
void CGModel::finalize()
{
  delete camera;
  delete scene;
  delete skybox;
  delete sceneProgram;
  delete skyboxProgram;
}

//
// FUNCIÓN: CGModel::resize(int w, int h)
//
// PROPÓSITO: Asigna el viewport y el clipping volume
//
void CGModel::resize(int w, int h)
{
  double fov = glm::radians(15.0);
  double sin_fov = sin(fov);
  double cos_fov = cos(fov);
  if (h == 0) h = 1;
  GLfloat aspectRatio = (GLfloat)w / (GLfloat)h;
  GLfloat wHeight = (GLfloat)(sin_fov * 0.2 / cos_fov);
  GLfloat wWidth = wHeight * aspectRatio;

  glViewport(0, 0, w, h);
  projection = glm::frustum(-wWidth, wWidth, -wHeight, wHeight, 0.2f, 400.0f);
}

//
// FUNCIÓN: CGModel::render()
//
// PROPÓSITO: Genera la imagen
//
void CGModel::render()
{
  glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glm::mat4 view = camera->ViewMatrix();
  skyboxProgram->Use();
  skybox->Draw(skyboxProgram, projection, view);
  sceneProgram->Use();
  scene->Draw(sceneProgram, projection, view);
}

//
// FUNCIÓN: CGModel::update()
//
// PROPÓSITO: Anima la escena
//
void CGModel::update()
{
  camera->MoveFront();
  CameraConstraints();
}

//
// FUNCIÓN: CGModel::key_pressed(int)
//
// PROPÓSITO: Respuesta a acciones de teclado
//
void CGModel::key_pressed(int key)
{
  switch (key)
  {
  case GLFW_KEY_UP:
    camera->TurnDown();
    break;
  case GLFW_KEY_DOWN:
    camera->TurnUp();
    break;
  case GLFW_KEY_LEFT:
    camera->TurnCCW();
    break;
  case GLFW_KEY_RIGHT:
    camera->TurnCW();
    break;
  case GLFW_KEY_S:
    camera->SetMoveStep(0.0f);
    break;
  case GLFW_KEY_RIGHT_BRACKET:
  case GLFW_KEY_KP_ADD: 
    camera->SetMoveStep(camera->GetMoveStep() + 0.1f);
    break;
  case GLFW_KEY_MINUS:
  case GLFW_KEY_KP_SUBTRACT:
    camera->SetMoveStep(camera->GetMoveStep() - 0.1f);
    break;
  case GLFW_KEY_Q:
    camera->SetMoveStep(0.1f);
    camera->MoveUp();
    camera->SetMoveStep(0.0f);
    break;
  case GLFW_KEY_A:
    camera->SetMoveStep(0.1f);
    camera->MoveDown();
    camera->SetMoveStep(0.0f);
    break;
  case GLFW_KEY_O:
    camera->SetMoveStep(0.1f);
    camera->MoveLeft();
    camera->SetMoveStep(0.0f);
    break;
  case GLFW_KEY_P:
    camera->SetMoveStep(0.1f);
    camera->MoveRight();
    camera->SetMoveStep(0.0f);
    break;
  case GLFW_KEY_K:
    camera->TurnLeft();
    break;
  case GLFW_KEY_L:
    camera->TurnRight();
    break;
  }
}

//
// FUNCIÓN: CGModel:::mouse_button(int button, int action)
//
// PROPÓSITO: Respuesta del modelo a un click del ratón.
//
void CGModel::mouse_button(int button, int action)
{
}

//
// FUNCIÓN: CGModel::mouse_move(double xpos, double ypos)
//
// PROPÓSITO: Respuesta del modelo a un movimiento del ratón.
//
void CGModel::mouse_move(double xpos, double ypos)
{
}

//
// FUNCIÓN: CGModel::CameraConstraints()
//
// PROPÓSITO: Limita el movimiento de la cámara a una cierta zona
//
void CGModel::CameraConstraints()
{
  glm::vec3 pos = camera->GetPosition();
  int constraint = 0;
  if (pos.y < 1.0f) { pos.y = 1.0f; constraint = 1; }
  if (pos.y > 40.0f) { pos.y = 40.0f; constraint = 1; }
  if (pos.x > 100.0f) { pos.x = 100.0f; constraint = 1; } 
  if (pos.x < -100.0f) { pos.x = -100.0f; constraint = 1; }
  if (pos.z > 100.0f) { pos.z = 100.0f; constraint = 1; }
  if (pos.z < -100.0f) { pos.z = -100.0f; constraint = 1; }
  if (constraint == 1)
  {
    camera->SetPosition(pos.x, pos.y, pos.z);
    camera->SetMoveStep(0.0f);
  }
}