Escuela Técnica Superior de Ingeniería

 

Grado en Ingeniería Informática

Animación por Ordenador

Curso 2025/2026

 

Práctica 3a

Creación del Renderpass

 

Objetivos

 

Crea la clase GERenderingContext y define el renderpass necesario para generar la imagen.

 

 

La estructura VkRenderPass

 

Todo proceso de dibujo en Vulkan debe estar contenido en un objeto VkRenderPass, que define el conjunto de pasadas de pipelines necesarias para generar el dibujo. Estas pasadas se conocen como subpasses. Incluso si solo se realiza una pasada es necesario crear un objeto VkRenderPass. Para crear el objeto VkRenderPass se utiliza la función vkCreateRenderPass().

VkResult vkCreateRenderPass (
  VkDevice                      device,
  const VkRenderPassCreateInfo* pCreateInfo,
  const VkAllocationCallbacks*  pAllocator,
  VkRenderPass*                 pRenderPass);

Para destruir un objeto VkRenderPass se usa la función vkDestroyRenderPass().

void vkDestroyRenderPass(
  VkDevice                     device,
  VkRenderPass                 renderPass,
  const VkAllocationCallbacks* pAllocator);

El proceso de renderizado trabaja sobre una estructura de memoria llamada framebuffer. Cada framebuffer contiene tres arrays encargados de almacenar la información de los píxeles. La imagen propiamente dicha corresponde al buffer en el que se almacena el color (color buffer), pero el framebuffer contiene dos arrays más dedicados a almacenar la profundidad de cada píxel (depth buffer) y la información auxiliar (stencil buffer). La configuración del renderpass se introduce en una estructura de tipo VkRenderPassCreateInfo.

typedef struct VkRenderPassCreateInfo {
  VkStructureType                sType;
  const void*                    pNext;
  VkRenderPassCreateFlags        flags;
  uint32_t                       attachmentCount;
  const VkAttachmentDescription* pAttachments;
  uint32_t                       subpassCount;
  const VkSubpassDescription*    pSubpasses;
  uint32_t                       dependencyCount;
  const VkSubpassDependency*     pDependencies;
} VkRenderPassCreateInfo;

El campo sType debe tener el valor VK_STRUCTURE_TYPE_RENDERPASS_CREATE_INFO.

El campo pNext debe dejarse nulo.

El campo flags debe ser cero.

Cada renderpass debe describir un conjunto de ataduras (attachments) que definen la forma en la que se va a leer y almacenar los framebuffers. El campo pAttachments contiene una lista describiendo estas ataduras. Para ello se utilizan estructuras de tipo VkAttachmentDescription. El campo attachmentCount indica el tamaño de esta lista.

El campo subpassCount indica el número de subpases que formarán el renderpass mientras que el campo pSubpasses contiene la lista con las descripciones de estos subpases. Las descripciones se detallan en estructuras de tipo VkSubpassDescription.

Cuando un subpass necesita utilizar información generada por un subpass previo es necesario definir una dependencia. El campo dependencyCount define el número de dependencias definidas en el renderpass. El campo pDependencies contiene el array de dependencias descritas mediante estructuras VkSubpassDependency.

La estructura VkAttachmentDescription tiene el siguiente contenido:

typedef struct VkAttachmentDescription {
  VkAttachmentDescriptionFlags flags;
  VkFormat                     format;
  VkSampleCountFlagBits        samples;
  VkAttachmentLoadOp           loadOp;
  VkAttachmentStoreOp          storeOp;
  VkAttachmentLoadOp           stencilLoadOp;
  VkAttachmentStoreOp          stencilStoreOp;
  VkImageLayout                initialLayout;
  VkImageLayout                finalLayout;
} VkAttachmentDescription;

El campo flags no se utiliza en estos momentos.

El campo format debe conicidir con el formato utilizado en las imágenes del SwapChain.

El campo samples define las opciones de multisampleado que deben coincidir con las asignadas en el pipeline.

El campo loadOp indica la actuación a realizar sobre el buffer de profundidad al comenzar el renderpass. Los valores pueden ser VK_ATTACHMENT_LOAD_OP_LOAD (para cargar como entrada el buffer que se generó como salida en la generación anterior), VK_ATTACHMENT_LOAD_OP_CLEAR (para limpiar el buffer de profundidad al comenzar el renderpass) o VK_ATTACHMENT_LOAD_OP_DONT_CARE (cuando resulte indiferente el valor inicial del buffer).

El campo storeOp indica la actuación a realizar sobre el buffer de profundidad al terninar el renderpass. Los valores aceptados son VK_ATTACHMENT_STORE_OP_STORE (para almacenar los valores si desean utilizarse en la próxima iteración) o VK_ATTACHMENT_STORE_OP_DONT_CARE (cuando no sea necesario almacenarlos).

El campo stencilLoadOp define la actuación a realizar sobre el stencil buffer al comenzar el renderpass. Admite los mismos valores que los aceptados por el campo loadOp.

El campo stencilStoreOp define la actuación a realizar sobre el stencil buffer al terminar el renderpass. Admite los mismos valores que los aceptados por el campo storeOp.

El campo initialLayout describe la forma en que se va a tratar inicialmente el color buffer. El tipo de dato VkImageLayout permite definir muchísimas formas de tratamiento, pero la mayoría de esos valores tienen sentido en otro ámbito. Si la forma inicial de la imagen es indiferente porque será totalmente creada a lo largo del renderizado, el valor a asignar es VK_IMAGE_LAYOUT_UNDEFINED.

El campo finalLayout describe la forma en que se tratará el color buffer tras el proceso de renderizado. Para volcar este color buffer sobre una superficie por medio de la extensión de presentación se utiliza el valor VK_IMAGE_LAYOUT_PRESENT_SRC_KHR.

La estructura VkSubpassDescription se utiliza para describir un subpase y tiene el siguiente contenido:

typedef struct VkSubpassDescription {
  VkSubpassDescriptionFlags    flags;
  VkPipelineBindPoint          pipelineBindPoint;
  uint32_t                     inputAttachmentCount;
  const VkAttachmentReference* pInputAttachments;
  uint32_t                     colorAttachmentCount;
  const VkAttachmentReference* pColorAttachments;
  const VkAttachmentReference* pResolveAttachments;
  const VkAttachmentReference* pDepthStencilAttachment;
  uint32_t                     preserveAttachmentCount;
  const uint32_t*              pPreserveAttachments;
} VkSubpassDescription;

El campo flags no se utiliza en estos momentos.

El campo pipelineBindPoint indica el tipo de pipeline que se asociará al subpass. Típicamente será VK_PIPELINE_BIND_POINT_GRAPHICS para generar gráficos o VK_PIPELINE_BIND_POINT_COMPUTE para pipelines de tipo computación aunque también se están añadiendo extensiones para identificar pipelines de ray tracing.

Los campos de tipo VkAttachmentReference permiten definir el formato utilizado en el almacenamiento de algunas variables utilizadas en el fragment shader. Esta estructura tiene un campo attachment para indicar el índice de la variable y un campo layout para indicar el formato de tipo VkImageLayout.

El campo pInputAttachments describe el formato de las entradas del fragment shader decoradas con un InputAttachmentIndex. El campo  inputAttachmentCount indica el número de ataduras de entrada incluidas en pInputAttachments.

El campo pColorAttachments describe el formato de salida para el color buffer del frambuffer. Se pueden configurar fragment shaders con varias salidas, que corresponderán a las diferentes ataduras descritas en este campo. El campo  colorAttachmentCount indica el número de ataduras que contiene pColorAttachments.

El campo pResolveAttachments describe las ataduras vinculadas a la operación resolve que se ejecuta en el multisampleado. Si se utiliza debe tener el mismo tamaño que pColorAttachments.

El campo pDepthStencilAttachment describe las ataduras asociadas al depth buffer y al stencil buffer.

Normalmente las ataduras no se conservan al pasar de un subpase a otro. Para preservar algunas ataduras se indican sus índices en el array pPreserveAttachments. El tamaño de este array se define en preserveAttachmentCount.

La estructura VkSubpassDependency define las dependencias entre los subpases y tiene el siguiente contenido:

typedef struct VkSubpassDependency {
  uint32_t             srcSubpass;
  uint32_t             dstSubpass;
  VkPipelineStageFlags srcStageMask;
  VkPipelineStageFlags dstStageMask;
  VkAccessFlags        srcAccessMask;
  VkAccessFlags        dstAccessMask;
  VkDependencyFlags    dependencyFlags;
} VkSubpassDependency;

El campo srcSubpass es el índice del subpase origen de la dependencia.

El campo dstSubpass es el índice del subpase de destino de la dependencia.

El campo srcStageMask indica las etapas del pipeline origen que tienen la dependencia, es decir, que deben finalizar antes de que comiencen a ejecutarse las del destino.

El campo dstStageMask indica las etapas del pipeline de destino que tienen la dependencia, es decir, que no pueden comenzar hasta que no hayan finalizado las de origen.

El campo srcAccessMask describe el tipo de dependencia sobre las estapas de origen, por ejemplo, que no se pueda leer o que no se pueda escribir en determinado resultado del pipeline de origen.

El campo dstAccessMask describe el tipo de dependencia sobre las etapas de destino.

El campo dependencyFlags indica la forma en que se produce la dependencia. Puede ser a nivel de framebuffer, a nivel de vista o incluso a nivel de dispositivo.

 

 

La clase GERenderingContext

 

A partir de ese proyecto vamos a añadir una nnueva clase, denominada GERenderingContext, que contiene la descripción de un proceso de renderizado. Inicialmente está formada por el campo renderPass y otros campos auxiliares que se toman del objeto GEDrawingContext. Los campos auxiliares son imageCount (número de imágenes de la swapchain), format (formato de las imágenes de la swapchain) y extent (tamaño de las imágenes de la swapchain). El contenido del fichero de cabecera de la clase es el siguiente.

#pragma once

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

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

private:
  VkFormat format;
  VkExtent2D extent;
  VkRenderPass renderPass;

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

private:
  // Métodos de creación de componentes
  void createRenderPass(GEGraphicsContext* gc);
};

Los métodos de la clase, inicialmente, son:

El código de CAVulkanState se modifica para añadir la creación del objeto VkRenderPass al constructor de la clase y la destrucción del objeto VkRenderPass a su destructor. Además se añade la función createRenderPass() que genera un objeto VkRenderPass con un único subpase.


#include "GERenderingContext.h"

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

//
// FUNCIÓN: GERenderingContext::GERenderingContext(...)
//
// PROPÓSITO: Crea un contexto de renderizado (renderpass) 
//
GERenderingContext::GERenderingContext(GEGraphicsContext* gc, GEDrawingContext* dc)
{
  imageCount = dc->getImageCount();
  format = dc->getFormat();
  extent = dc->getExtent();
  createRenderPass(gc);

  std::cout << "Render pass created!" << std::endl;
}

//
// FUNCIÓN: GERenderingContext::destroy(GEGraphicsContext* gc)
//
// PROPÓSITO: Crea el estado de Vulkan
//
void GERenderingContext::destroy(GEGraphicsContext* gc)
{
  vkDestroyRenderPass(gc->device, renderPass, nullptr);
}

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

//
// FUNCIÓN: GERenderingContext::createRenderPass(GEGraphicsContext* gc)
//
// PROPÓSITO: Crea los pasos de renderizado
//
void GERenderingContext::createRenderPass(GEGraphicsContext* gc)
{
  VkAttachmentDescription colorAttachment = {};
  colorAttachment.format = format;
  colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
  colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
  colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
  colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
  colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
  colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
  colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;

  VkAttachmentDescription depthAttachment = {};
  depthAttachment.format = gc->findDepthFormat();
  depthAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
  depthAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
  depthAttachment.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
  depthAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
  depthAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
  depthAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
  depthAttachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;

  VkAttachmentReference colorAttachmentRef = {};
  colorAttachmentRef.attachment = 0;
  colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

  VkAttachmentReference depthAttachmentRef{};
  depthAttachmentRef.attachment = 1;
  depthAttachmentRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;

  VkSubpassDescription subpass = {};
  subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
  subpass.colorAttachmentCount = 1;
  subpass.pColorAttachments = &colorAttachmentRef;
  subpass.pDepthStencilAttachment = &depthAttachmentRef;

  VkSubpassDependency dependency = {};
  dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
  dependency.dstSubpass = 0;
  dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | 
                            VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
  dependency.srcAccessMask = 0;
  dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | 
                            VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
  dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | 
                             VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;

  VkAttachmentDescription attachment[] = { colorAttachment, depthAttachment };

  VkRenderPassCreateInfo renderPassInfo = {};
  renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
  renderPassInfo.attachmentCount = 2;
  renderPassInfo.pAttachments = attachment;
  renderPassInfo.subpassCount = 1;
  renderPassInfo.pSubpasses = &subpass;
  renderPassInfo.dependencyCount = 1;
  renderPassInfo.pDependencies = &dependency;

  if (vkCreateRenderPass(gc->device, &renderPassInfo, nullptr, &renderPass) 
                                                                   != VK_SUCCESS)
  {
    throw std::runtime_error("failed to create render pass!");
  }
}

 

 

Modificaciones de la clase GEApplication

 

La clase GEApplication se modifica para añadir como campo el objeto GERenderingContext con la descripción del proceso de renderizado a ejecutar en la aplicación.

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

private:
  GLFWwindow* window;
  GEWindowPosition windowPos;
  GEGraphicsContext* gc;
  GEDrawingContext* dc;
  GECommandContext* cc;
  GERenderingContext* rc;

  ...
};

La modificación afecta a los métodos run() y cleanup().

//
// FUNCIÓN: GEApplication::run()
//
// PROPÓSITO: Ejecuta la aplicación
//
void GEApplication::run()
{
  this->window = initWindow();
  this->windowPos = initWindowPos();
  this->gc = new GEGraphicsContext(window);
  this->dc = new GEDrawingContext(this->gc, this->windowPos);
  this->cc = new GECommandContext(this->gc, this->dc->getImageCount());
  this->rc = new GERenderingContext(this->gc, this->dc);

  mainLoop();

  cleanup();
}

//
// FUNCIÓN: GEApplication::cleanup()
//
// PROPÓSITO: Libera los recursos y finaliza la aplicación
//
void GEApplication::cleanup()
{
  rc->destroy(gc);
  cc->destroy(gc);
  dc->destroy(gc);
  delete rc;
  delete cc;
  delete dc;
  delete gc;
  glfwDestroyWindow(window);
  glfwTerminate();
}

 

 

Aspecto final

 

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

Captura 1