Escuela Técnica Superior de Ingeniería

 

Grado en Ingeniería Informática

Animación por Ordenador

Curso 2025/2026

 

Práctica 1b

Creación de una ventana gráfica con GLFW

 

Objetivos

 

El objetivo de este proyecto es añadir a la aplicación una ventana gráfica por medio de la biblioteca GLFW.

 

 

La biblioteca GLFW

 

GLFW es una biblioteca de código abierto dedicada a la gestión de ventanas con soporte para OpenGL, OpenGL ES y Vulkan. Se trata de una biblioteca escrita en C adaptada a múltiples plataformas (MS-Windows, MacOS, X11, Wayland). Las funciones de la biblioteca dan soporte a múltiples ventanas, monitores y eventos de teclado, ratón, joystick e incluso mando de consolas.

La web oficial de la biblioteca es https://www.glfw.org/. En esta página se puede encontrar toda la documentación de la biblioteca así como la zona de descarga. Para el desarrollo de estas prácticas se ha incluido ya la distribución de GLFW en el directorio \GameEngine\Tools.

Para utilizar GLFW hay que comenzar ejecutando la función glfwInit(). Al terminar la aplicación debe ejecutarse glfwTerminate() para liberar todos los recursos que pueda haberse creado. La biblioteca define la estructura GLFWwindow para describir las ventanas. Para crear una ventana se utiliza la función glfwCreateWindow() y para destruirla se usa glfwDestroyWindow(). Para que la ventana gráfica soporte Vulkan es necesario definir la macro GLFW_INCLUDE_VULKAN antes de importar la bibliteca de GLFW.

Las respuestas a los distintos eventos se configura por medio de punteros a funciones. Típicamente estas funciones deben acceder a la información de la aplicación que ha creado la ventana. Para ello se utiliza la función  glfwSetWindowUserPointer() que permite almacenar una referencia a un objeto de cualquier tipo, lo que permite acceder posteriormente a este objeto por medio de la función glfwGetWindowUserPointer().

 


La estructura GEWindowsPosition

 

La aplicación gráfica que vamos a desarrollar está generada en modo consola. Esto quiere decir que la ejecución de la aplicación va a abrir una consola de texto, que nos servirá para mostrar información, y una ventana gráfica, desarrollada por la biblioteca GLFW. Las aplicaciones gráficas (por ejemplo, los videojuegos) suelen ejecutarse a pantalla completa. Esto significa que el área de dibujo ocupa todo el monitor. En estas prácticas vamos a permitir que nuestra aplicación se ejecute a pantalla completa o en modo ventana. Utilizaremos la tecla F12 para cambiar de modo de presentación. Para poder trabajar en ambos modos es necesario almacenar la información del tamaño y posición de la ventana, así como el tamaño del monitor y un estado para indicar el modo de presentación activo. Esta información se ha encapsulado en una estructura llamada GEWindowsPosition que se ha añadido al proyecto. La definición de la estructura es la siguiente.

#pragma once

typedef struct {
    int width;
    int height;
    int Xpos;
    int Ypos;
    int screenWidth;
    int screenHeight;
    bool fullScreen;
} GEWindowPosition;

 

 

La clase GEApplication

 

En este proyecto se ha modificado la clase GEApplication para que incluya la creación y gestión de la ventana sobre la que generaremos los gráficos.  El campo window permite almacenar la referencia a la ventana generada por la biblioteca GLFW. El campo windowPos contiene la información de la posición de la ventana y el modo de presentación. A continuación se muestra la declaración de esta clase.

#pragma once

#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#include "GEWindowPosition.h"

const int WIDTH = 800;
const int HEIGHT = 600;

//
// CLASE: GEApplication
//
// DESCRIPCIÓN: Clase que crea y lanza la aplicación gráfica.
//
class GEApplication
{
public:
    void run();

private:
    GLFWwindow* window;
    GEWindowPosition windowPos;

    // Métodos principales
    GLFWwindow* initWindow();
    GEWindowPosition initWindowPos();
    void mainLoop();
    void draw();
    void cleanup();
    void swapFullScreen();
    void resize();

    // Respuesta a eventos
    static void keyCallback(GLFWwindow* w, int k, int scancode, int act, int mods);
    static void mouseButtonCallback(GLFWwindow* w, int button, int act, int mods);
    static void cursorPositionCallback(GLFWwindow* w, double xpos, double ypos);
    static void framebufferResizeCallback(GLFWwindow* w, int width, int height);
};

La clase GEApplication tiene los siguientes métodos:

  • run(): único método público, encargado de ejecutar la aplicación. Este método consiste en inicializar la aplicación y quedarse en un bucle de gestión de eventos de la ventana hasta recibir la orden de cierre de la aplicación.

  • initWindow(): es el encargado de crear y configurar la ventana por medio de las funciones de la biblioteca GLFW.

  • initWindowPos(): inicializa la posición de la ventanay el modo de presentación.

  •  mainLoop(): es el bucle principal de la aplicación y consiste en un bucle infinito encargado de recibir y gestionar los eventos de la ventana y actualizar y mostrar la representación gráfica.

  • draw(): de momento no tiene contenido ya que para generar los gráficos necesitamos tener configurado todo el proceso de renderizado.

  • cleanup(): se ejecuta al cierre de la aplicación y consiste en liberar los recursos utilizados por la aplicación.

  • swapFullScreen(): cambia el modo de presentación (pantalla completa o ventana).

  • resize(): cambia el tamaño de la ventana.Cuando se gestione la aplicación gráfica esto supone cambiar el tamaño de las imágenes a crear y la perspectiva.

  • métodos Callback(): son los encargados de dar respuesta a los diferentes eventos de la ventana. Para ello se almacena en la ventana una referencia al objeto GEApplication que se puede obtener mediante la función glfwGetWindowUserPointer().

El código de la clase es el siguiente.

#include "GEApplication.h"
#include <windows.h>
#include <iostream>
#include <vector>
#include <glm/common.hpp>

//
// FUNCIÓN: GEApplication::run()
//
// PROPÓSITO: Ejecuta la aplicación
//
void GEApplication::run()
{
  this->window = initWindow();
  this->windowPos = initWindowPos();

  mainLoop();

  cleanup();
}

//
// FUNCIÓN: GEApplication::initWindow()
//
// PROPÓSITO: Inicializa la ventana
//
GLFWwindow* GEApplication::initWindow()
{
  glfwInit();

  glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
  GLFWwindow* w = glfwCreateWindow(WIDTH, HEIGHT, "Game Engine", nullptr, nullptr);
  glfwSetWindowUserPointer(w, this);
  glfwSetFramebufferSizeCallback(w, framebufferResizeCallback);
  glfwSetKeyCallback(w, keyCallback);
  glfwSetCursorPosCallback(w, cursorPositionCallback);
  glfwSetMouseButtonCallback(w, mouseButtonCallback);

  return w;
}

//
// FUNCIÓN: GEApplication::initWindowPos()
//
// PROPÓSITO: Inicializa la posición de la ventana
//
GEWindowPosition GEApplication::initWindowPos()
{
  GLFWmonitor* monitor = glfwGetPrimaryMonitor();
  const GLFWvidmode* mode = glfwGetVideoMode(monitor);

  GEWindowPosition wp = {};
  wp.fullScreen = false;
  glfwGetWindowSize(window, &wp.width, &wp.height);
  glfwGetWindowPos(window, &wp.Xpos, &wp.Ypos);
  wp.screenWidth = mode->width;
  wp.screenHeight = mode->height;
  return wp;
}


//
// FUNCIÓN: GEApplication::mainLoop()
//
// PROPÓSITO: Bucle principal que procesa los eventos de la aplicación
//
void GEApplication::mainLoop()
{
  while (!glfwWindowShouldClose(window))
  {
    glfwPollEvents();
    draw();
  }
}

//
// FUNCIÓN: GEApplication::draw()
//
// PROPÓSITO: Lanza la generación del dibujo
//
void GEApplication::draw()
{
}

//
// FUNCIÓN: GEApplication::cleanup()
//
// PROPÓSITO: Libera los recursos y finaliza la aplicación
//
void GEApplication::cleanup()
{
  glfwDestroyWindow(window);
  glfwTerminate();
}

//
// FUNCIÓN: GEApplication::swapFullScreen()
//
// PROPÓSITO: Dibuja la ventana a pantalla completa o a tamaño configurable
//
void GEApplication::swapFullScreen()
{
  if (!windowPos.fullScreen)
  {
    glfwGetWindowSize(window, &windowPos.width, &windowPos.height);
    glfwGetWindowPos(window, &windowPos.Xpos, &windowPos.Ypos);
    windowPos.fullScreen = true;
    GLFWmonitor* monitor = glfwGetPrimaryMonitor();
    const GLFWvidmode* mode = glfwGetVideoMode(monitor);
    glfwSetWindowMonitor(window, monitor, 0, 0, mode->width, mode->height, 
                                                             mode->refreshRate);
  }
  else
  {
    windowPos.fullScreen = false;
    glfwSetWindowMonitor(window, nullptr, windowPos.Xpos, windowPos.Ypos, 
                                          windowPos.width, windowPos.height, NULL);
  }
}

//
// FUNCIÓN: GEApplication::framebufferResizeCallback(GLFWwindow* window, int width, 
//                                                                       int height)
//
// PROPÓSITO: Reconstruye los objetos con el nuevo tamaño de ventana
//
void GEApplication::resize()
{
  if (!windowPos.fullScreen)
  {
    glfwGetWindowSize(window, &windowPos.width, &windowPos.height);
    glfwGetWindowPos(window, &windowPos.Xpos, &windowPos.Ypos);
  }
}

//
// FUNCIÓN: GEApplication::keyCallback(GLFWwindow* window, int key, int scancode, 
//                                                         int action, int mods)
//
// PROPÓSITO: Respuesta a un evento de teclado sobre la aplicación
//
void GEApplication::keyCallback(GLFWwindow* window, int key, int scancode, 
                                                    int action, int mods)
{
  GEApplication* app = (GEApplication*)glfwGetWindowUserPointer(window);
  if (action == GLFW_PRESS || action == GLFW_REPEAT)
  {
    if (key == GLFW_KEY_F12) app->swapFullScreen();
  }
}

//
// FUNCIÓN: GEApplication::mouseButtonCallback(GLFWwindow* window, int button, 
//                                                         int action, int mods)
//
// PROPÓSITO: Respuesta a un evento de ratón sobre la aplicación
//
void GEApplication::mouseButtonCallback(GLFWwindow* window, int button, 
                                                            int action, int mods)
{
}

//
// FUNCIÓN: GEApplication::cursorPositionCallback(GLFWwindow* window, double xpos, 
//                                                                    double ypos)
//
// PROPÓSITO: Respuesta a un evento de movimiento del cursor sobre la aplicación
//
void GEApplication::cursorPositionCallback(GLFWwindow* window, double xpos, 
                                                               double ypos)
{
}

//
// FUNCIÓN: GEApplication::framebufferResizeCallback(GLFWwindow* window, int width, 
//                                                                       int height)
//
// PROPÓSITO: Respuesta a un evento de redimensionamiento de la ventana de la 
//            aplicación
//
void GEApplication::framebufferResizeCallback(GLFWwindow* window, int width, 
                                                                  int height)
{
  GEApplication* app = (GEApplication*)glfwGetWindowUserPointer(window);
  if (height != 0)
  {
    app->resize();
  }
}

 

 

Aspecto final

 

Una vez generado los ficheros anteriores la aplicación puede ser compilada y ejecutada. A continuación se muestra el aspecto de la ventana principal de la aplicación.

Figura17