Escuela Técnica Superior de Ingeniería

 

Grado en Ingeniería Informática

Animación por Ordenador

Curso 2025/2026

 

Práctica 3d

Creación de los Framebuffers

 

Objetivo

 

Añade a la clase GERenderingContext la definición de los framebuffers asociados a cada imagen de la swapchain.

 

 

La estructura VkFramebuffer

 

El proceso de renderizado tiene como resultado una estructura denominada framebuffer. Esta estructura esta formada por un conjunto de buffers que contienen información asociada a los píxeles de la imagen generada. La imagen propiamente dicha corresponde al color de cada píxel, lo que se conoce como color buffer. El framebuffer almacena también información sobre la profundidad de cada píxel (la coordenada Z del fragmento dibujado en ese píxel), lo que se denomina depth buffer. El framebuffer contiene también otro buffer auxiliar que permite almacenar otra información asociada a los framentos, que se conoce como stencil buffer. Toda esta información se representa por medio de un objeto VkFramebuffer. Al trabajar sobre un swapchain hay que definir un framebuffer para cada VkImageView del swapchain. Para crear un ojeto VkFramebuffer se utiliza la función vkCreateFramebuffer().

VkResult vkCreateFramebuffer(
  VkDevice                       device,
  const VkFramebufferCreateInfo* pCreateInfo,
  const VkAllocationCallbacks*   pAllocator,
  VkFramebuffer*                 pFramebuffer);

Para destruir un objeto VkFramebuffer se usa la función vkDestroyFramebuffer().

void vkDestroyFramebuffer(
  VkDevice                     device,
  VkFramebuffer                framebuffer,
  const VkAllocationCallbacks* pAllocator);

La configuración del framebuffer se introduce en una estructura de tipo VkFramebufferCreateInfo .

typedef struct VkFramebufferCreateInfo {
  VkStructureType          sType;
  const void*              pNext;
  VkFramebufferCreateFlags flags;
  VkRenderPass             renderPass;
  uint32_t                 attachmentCount;
  const VkImageView*       pAttachments;
  uint32_t                 width;
  uint32_t                 height;
  uint32_t                 layers;
} VkFramebufferCreateInfo;

El campo sType debe tener el valor VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO.

El campo pNext debe dejarse nulo.

El campo flags suele ser cero. A partir de la versión Vulkan 1.2 se admite el valor VK_FRAMEBUFFER_CREATE_IMAGELESS_BIT para indicar que el framebuffer a crear no debe incluir el color buffer.

El campo renderpass debe incluir la referencia a un VkRenderPass compatible con el framebuffer. Normalmente se introduce la referencia al renderpass definido en la aplicación.

El campo pAttachments identifica los objetos VkImageView con el que se relacionará el framebuffer. Un proceso de renderizado puede definir varias variables de salida en el fragment shader, lo que provoca que el framebuffer deba tener varios buffers vinculados a cada pixel y cada uno se deberá vincular a una vista. Por esa razón este campo está declarado como un array. El campo attachmentCount indica el tamaño de este array.

Los campos width, height y layers indican el tamaño de los buffers, que deben coincidir con el tamaño de las vistas.

 

 

El buffer de profundidad

 

El buffer de profundidad es necesario para que al dibujar un pixel se tenga en cuenta si ese píxel se ha rellenado anteriormente como parte de una primitiva que se encuentre más cerca del observador. Antes de lanzar el Fragment Shader sobre un pixel se estudia el valor de profundidad almacenado en las coordenadas de ese pixel, que deben corresponder a la profundidad almacenada en la ejecución de una primitiva tratada con anterioridad. Generalmente el test de profundidad consiste en no lanzar el Fragment Shader si el pixel a dibujar está más alejado que el pixel ya dibujado. Por tanto, el buffer de profundidad (depth buffer) es una estructura paralela a la utilizada para la imagen (color buffer) pero dedicada a almacenar otro tipo de datos.

En Vulkan existen dos tipos de recursos para almacenar datos: los objetos VkBuffer y los objetos VkImage. Los objetos VkBuffer se utilizan para almacenar bloques de datos sin una estructura prefijada. Es el tipo de recurso que se utiliza para almacenar los atributos de los vértices, los índices de vértices que forman las primitivas o los valores de las variables uniformes de los shaders. Los objetos VkImage permiten almacenar datos con una estructura que incluye información sobre su formato y dimensiones y tienen procedimientos específicos para leer y escribir datos sobre ellos. Los buffers de profundidad se almacenan en objetos de tipo VkImage ya que deben tratarse de la misma forma que los color buffer asociados.

Los objetos VkBuffer y VkImage describen los bloques de datos a almacenar pero no definen la memoria sobre la que serán almacenados. Para asignar esa memoria es necesario definir objetos VkDeviceMemory y vincularlos a estos recursos. Para poder usar el objeto VkImage es necesario crear un vista sobre ese objeto. Las vistas son objetos VkImageView que contienen información adicional necesaria para poder interactuar con el objeto VkImage.

Al crear la swapchain se crean de manera automática los objetos VkImage y VkDeviceMemory que almacenan las imágenes a presentar en la superficie. Como hemos visto es necesario crear explicitamente los objetos VkImageView para poder acceder a estas imágenes. Para utilizar los buffers de profundidad hay que crear explicitamente los objetos VkImage, VkDeviceMemory y VkImageView.

Los objetos VkImage se crean con la función vkCreateImage() que se describe a continuación.

VkResult vkCreateImage (
  VkDevice                     device,
  const VkImageCreateInfo*     pCreateInfo,
  const VkAllocationCallbacks* pAllocator,
  VkImage*                     pImage);

La definición de la estructura VkImageCreateInfo es la siguiente

typedef struct VkImageCreateInfo {
  VkStructureType       sType;
  const void*           pNext;
  VkImageCreateFlags    flags;
  VkImageType           imageType;
  VkFormat              format;
  VkExtent3D            extent;
  uint32_t              mipLevels;
  uint32_t              arrayLayers;
  VkSampleCountFlagBits samples;
  VkImageTiling         tiling;
  VkImageUsageFlags     usage;
  VkSharingMode         sharingMode;
  uint32_t              queueFamilyIndexCount;
  const uint32_t*       pQueueFamilyIndices;
  VkImageLayout         initialLayout;
} VkImageCreateInfo;

Algunos de los campos incluidos en esta estructura coinciden con campos incluidos en la creación de swapchains ya que son necesarios para crear las imágenes que forman la swapchain. Puesto que las imágenes de la swapchain y los buffers de profundidad son estructuras paralelas muchos de estos campos deben coincidir entre ellas.

El campo sType debe tener el valor VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO.

Los campos pNext y flags pueden dejarse nulos. Existen posibles valores para el campo flags pero no son necesarios para crear buffers de profundidad.

El campo imageType indica el tipo de imagen. Las imágenes bidimensionales se definen como VK_IMAGE_TYPE_2D.

El campo format indica el formato utilizado para almacenar la información de cada pixel.

El campo extent almacena el tamaño en pixel de la imagen a crear. Para las imágenes a presentar y los buffers de profundidad la tercera componente debe ser 1.

El campo mipLevels especifica el número de niveles de mipmap de la imagen a crear. En este caso se usa un único nivel.

El campo arrayLayers indica el número de capas de la imagen. Para los buffers de profundidad se usa una única capa.

El campo tiling indica la forma de posicionar la imagen. Generalmente se utiliza el valor VK_IMAGE_TILING_OPTIMAL que deja en manos de la GPU este proceso.

El campo usage describe el uso que se le va a dar a la imagen. Para utilizarla como buffer de profundidad se usa el valor VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT.

El campo sharingMode indica si el objeto va a ser accesible desde comandos de diferentes colas (VK_SHARING_MODE_CONCURRENT) o solo desde una (VK_SHARING_MODE_EXCLUSIVE). Si el acceso va a ser concurrente hay que indicar cuantas familias de colas van a acceder a el (queueFamilyIndexCount) y la lista de esas familias (pQueueFamilyIndices).

El campo initialLayout, aunque de tipo VkImageLayout, solo puede tomar los valores VK_IMAGE_LAYOUT_UNDEFINED or the VK_IMAGE_LAYOUT_PREINITIALIZED. Para crear los buffers de profundidad se deja indifinido.

Una vez creado el buffer hay que asignar la memoria del dispositivo donde se almacenarán sus datos. Para esto hay que crear un objeto VkDeviceMemory por medio de la función vkAllocateMemory().

VkResult vkAllocateMemory(
  VkDevice                     device,
  const VkMemoryAllocateInfo*  pAllocateInfo,
  const VkAllocationCallbacks* pAllocator,
  VkDeviceMemory*              pMemory);

Para asignar la memoria hay que configurar el tipo de memoria a asignar por medio de una estructura VkMemoryAllocateInfo.

typedef struct VkMemoryAllocateInfo {
  VkStructureType sType;
  const void*     pNext;
  VkDeviceSize    allocationSize;
  uint32_t        memoryTypeIndex;
} VkMemoryAllocateInfo;

El campo sType debe ser

El campo pNext está reservado para un uso futuro.

El campo allocationSize indica el tamaño en bytes de la memoria a asignar.

El campo memoryTypeIndex expresa el tipo de memoria a asignar. Los dispositivos gráficos soportan diferentes tipos de memoria que pueden obtenerse mediante la función vkGetPhysicalDeviceMemoryProperties(). El valor a introducir en este campo corresponde al índice del tipo de memoria deseada de entre las soportadas por el dispositivo. Para los buffers de profundidad hay que escoger un tipo de memoria que sea especialmente rápida para accesos internos del dispositivo. Esto se corresponde a un tipo de memoria que contenga la propiedad VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT.

Una vez reservada la memoria, es necesario establecer la vinculación entre el buffer y la memoria por medio de la función vkBindImageMemory().

VkResult vkBindImageMemory(
  VkDevice       device,
  VkImage        image,
  VkDeviceMemory memory,
  VkDeviceSize   memoryOffset);

Para terminar de generar la configuración de los buffers de profundidad hay que crear un objeto VkImageView para controlar el acceso al buffer. El proceso de creación de vistas se explicó en el proyecto 2b

 

 

La clase GEDepthBuffer

 

Para generar los framebufers hay que crear todas las estructuras asociadas a los buffers de profundidad, de manera que cada framebuffer incluya una imagen del swapchain y un buffer de profundidad. La clase GEDepthBuffer se ha definido para almacenar la información asociada a un buffer de profundidad, es decir, la imagen, la memoria y la vista.

#pragma once

#include <vulkan/vulkan.h>
#include "GEGraphicsContext.h"

//
// CLASE: GEDepthBuffer
//
// DESCRIPCIÓN: Clase que describe un buffer de profundidad
//
class GEDepthBuffer
{
public:
  VkImage image;
  VkDeviceMemory imageMemory;
  VkImageView imageView;

  GEDepthBuffer(GEGraphicsContext* gc, VkExtent2D extent);
  void destroy(GEGraphicsContext* gc);
};

El constructor de la clase recibe como dato el tamaño de la imagen a crear y utiliza el contexto gráfico para crear la imagen, reservar la memoria y crear la vista.

#include "GEDepthBuffer.h"

#include <iostream>

//
// FUNCIÓN: GEDepthBuffer::GEDepthBuffer(GEGraphicsContext* gc, VkExtent2D extent)
//
// PROPÓSITO: Crea un buffer de profundidad (con su imagen, memoria y vista)
//
GEDepthBuffer::GEDepthBuffer(GEGraphicsContext* gc, VkExtent2D extent)
{
  VkImageCreateInfo imageInfo = {};
  imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
  imageInfo.imageType = VK_IMAGE_TYPE_2D;
  imageInfo.extent.width = extent.width;
  imageInfo.extent.height = extent.height;
  imageInfo.extent.depth = 1;
  imageInfo.mipLevels = 1;
  imageInfo.arrayLayers = 1;
  imageInfo.format = gc->findDepthFormat(); // format;
  imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
  imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
  imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
  imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
  imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

  if (vkCreateImage(gc->device, &imageInfo, nullptr, &image) != VK_SUCCESS)
  {
    throw std::runtime_error("failed to create depth buffer image!");
  }

  VkMemoryRequirements memRequirements;
  vkGetImageMemoryRequirements(gc->device, image, &memRequirements);

  VkMemoryAllocateInfo allocInfo = {};
  allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
  allocInfo.allocationSize = memRequirements.size;
  allocInfo.memoryTypeIndex = gc->findMemoryType(memRequirements.memoryTypeBits, 
                                              VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);

  if (vkAllocateMemory(gc->device, &allocInfo, nullptr, &imageMemory) != VK_SUCCESS)
  {
    throw std::runtime_error("failed to allocate depth buffer image memory!");
  }

  vkBindImageMemory(gc->device, image, imageMemory, 0);

  VkImageViewCreateInfo viewInfo = {};
  viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
  viewInfo.image = image;
  viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
  viewInfo.format = gc->findDepthFormat();
  viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
  viewInfo.subresourceRange.baseMipLevel = 0;
  viewInfo.subresourceRange.levelCount = 1;
  viewInfo.subresourceRange.baseArrayLayer = 0;
  viewInfo.subresourceRange.layerCount = 1;

  if (vkCreateImageView(gc->device, &viewInfo, nullptr, &imageView) != VK_SUCCESS)
  {
    throw std::runtime_error("failed to create depth buffer image view!");
  }
}

//
// FUNCIÓN: GEDepthBuffer::destroy(GEGraphicsContext* gc)
//
// PROPÓSITO: Destruye un buffer de profundidad
//
void GEDepthBuffer::destroy(GEGraphicsContext* gc)
{
  vkDestroyImageView(gc->device, imageView, nullptr);
  vkFreeMemory(gc->device, imageMemory, nullptr);
  vkDestroyImage(gc->device, image, nullptr);
}

 

 

Modificaciones de la clase GERenderingContext

 

La clase GERenderingContext se ha ampliado para incorporar los framebuffers. Hay que generar un framebuffer con su depthbuffer correspondiente para cada imagen de la swapchain. Por este motivo los campos framebuffers y depthbuffers se definen como vectores.

//
// CLASE: GERenderingContext
//
// DESCRIPCIÓN: Clase que describe un contexto de renderizado
//
class GERenderingContext
{
public:
  uint32_t imageCount;

private:
  VkFormat format;
  VkExtent2D extent;
  VkRenderPass renderPass;
  VkPipeline graphicsPipeline;
  VkDescriptorSetLayout descriptorSetLayout;
  VkPipelineLayout pipelineLayout;
  std::vector<GEDepthBuffer*> depthBuffers;
  std::vector<VkFramebuffer> framebuffers;
  VkViewport viewport;
  VkRect2D scissor;

public:
  GERenderingContext(GEGraphicsContext* gc, GEDrawingContext* dc,
                                            GEPipelineConfig* config);
  void destroy(GEGraphicsContext* gc);

private:
  // Métodos de creación de componentes
  void createRenderPass(GEGraphicsContext* gc);
  void createGraphicsPipeline(GEGraphicsContext* gc, GEPipelineConfig* config);
  void createDepthBuffers(GEGraphicsContext* gc);
  void createFramebuffers(GEGraphicsContext* gc, GEDrawingContext* dc);
  
  ...
}:

El constructor y el método destroy() deben incluir ahora la creación y destrucción de los dos nuevos campos. Los métodos

/////////////////////////////////////////////////////////////////////////////////
/////                                                                       /////
/////                            Métodos públicos                           /////
/////                                                                       /////
/////////////////////////////////////////////////////////////////////////////////

//
// FUNCIÓN: GERenderingContext::GERenderingContext()
//
// PROPÓSITO: Crea un contexto de renderizado (pipeline, renderpass y framebuffers) 
//
GERenderingContext::GERenderingContext(GEGraphicsContext* gc, 
                                       GEDrawingContext* dc, 
                                       GEPipelineConfig* config)
{
  imageCount = dc->getImageCount();
  format = dc->getFormat();
  extent = dc->getExtent();
  createRenderPass(gc);
  createGraphicsPipeline(gc, config);
  createDepthBuffers(gc);
  createFramebuffers(gc, dc);
}

//
// FUNCIÓN: GERenderingContext::destroy(GEGraphicsContext* gc)
//
// PROPÓSITO: Crea el estado de Vulkan
//
void GERenderingContext::destroy(GEGraphicsContext* gc)
{
  for (uint32_t i = 0; i < imageCount; i++)
  {
    vkDestroyFramebuffer(gc->device, framebuffers[i], nullptr);
    depthBuffers[i]->destroy(gc);
  }
  vkDestroyPipeline(gc->device, graphicsPipeline, nullptr);
  vkDestroyPipelineLayout(gc->device, pipelineLayout, nullptr);
  vkDestroyDescriptorSetLayout(gc->device, descriptorSetLayout, nullptr);
  vkDestroyRenderPass(gc->device, renderPass, nullptr);
}

/////////////////////////////////////////////////////////////////////////////////
/////                                                                       /////
/////                 Métodos de creación de los componentes                /////
/////                                                                       /////
/////////////////////////////////////////////////////////////////////////////////

//
// FUNCIÓN: GERenderingContext::createDepthBuffers()
//
// PROPÓSITO: Crea los buffers de profundidad
//
void GERenderingContext::createDepthBuffers(GEGraphicsContext* gc)
{
  depthBuffers.resize(imageCount);
  for (size_t i = 0; i < imageCount; i++)
  {
    depthBuffers[i] = new GEDepthBuffer(gc, extent);
  }
}

//
// FUNCIÓN: GERenderingContext::createFramebuffers()
//
// PROPÓSITO: Crea un Framebuffer para cada imagen del swapchain
//
void GERenderingContext::createFramebuffers(GEGraphicsContext* gc, 
                                            GEDrawingContext* dc)
{
  framebuffers.resize(imageCount);

  for (size_t i = 0; i < imageCount; i++)
  {
    VkImageView attachments[] = { dc->imageViews[i], depthBuffers[i]->imageView };

    VkFramebufferCreateInfo framebufferInfo{};
    framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
    framebufferInfo.renderPass = renderPass;
    framebufferInfo.attachmentCount = 2;
    framebufferInfo.pAttachments = attachments;
    framebufferInfo.width = extent.width;
    framebufferInfo.height = extent.height;
    framebufferInfo.layers = 1;

    if (vkCreateFramebuffer(gc->device, &framebufferInfo, nullptr, &framebuffers[i])
                                                                    != VK_SUCCESS)
    {
      throw std::runtime_error("failed to create framebuffer!");
    }
  }

  std::cout << "Framebuffers created!" << std::endl;
}

 

 

Aspecto final

 

El único cambio respecto a la aplicación es el mensaje presentado en consola.

Captura 4