Escuela Técnica Superior de Ingeniería

 

Grado en Ingeniería Informática

Animación por Ordenador

Curso 2025/2026

 

Práctica 3e

Generación de la imagen

 

Objetivo

 

Rellenar los buffers de comandos para generar las imágenes.

 

 

Uso de la estructura VkCommandBuffer

 

Para enviar a la GPU la orden de generar las imágenes siguiendo la configuración reflejada en los objetos VkRenderPass y VkPipeline hay que rellenar un buffer de comandos con los comandos adecuados. Para ello hay que inicializarlo, llenarlo de comandos y lanzarlo. Para inicializar el buffer se utiliza la función vkBeginCommandBuffer().

VkResult vkBeginCommandBuffer (
  VkCommandBuffer                 commandBuffer,
  const VkCommandBufferBeginInfo* pBeginInfo);

La configuración de esta función se realiza con una estructura VkCommandBufferBeginInfo.

typedef struct VkCommandBufferBeginInfo {
  VkStructureType                       sType;
  const void*                           pNext;
  VkCommandBufferUsageFlags             flags;
  const VkCommandBufferInheritanceInfo* pInheritanceInfo;
} VkCommandBufferBeginInfo;

El campo sType debe tener el valor VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO.

El campo pNext debe dejarse nulo.

El campo flags indica el uso que se puede dar al buffer. Si se deja nulo indica que se puede lanzar varias veces. Los otros valores son VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT (para indicar que solo se ejecutará una vez), VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT (para indicar que un buffer secundario se ejecutará completamente dentro de un subpase) y VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT (para indicar que se puede lanzar el buffer de comando de forma simultanea en varias colas).

El campo pInheritanceInfo se utiliza en los buffers secundarios para especificar las propiedades que se heredan del buffer primario.

Una vez inicializado el buffer de comandos hay que iniciar el renderpass por medio de la función vkCmdBeginRenderPass().

void vkCmdBeginRenderPass (
  VkCommandBuffer              commandBuffer,
  const VkRenderPassBeginInfo* pRenderPassBegin,
  VkSubpassContents            contents); 

La información contenida en la estructura VkRenderPassBeginInfo incluye las referencias al renderpass, al framebuffer, al área de dibujo y a el color utilizado para inicializar la imagen.

typedef struct VkRenderPassBeginInfo {
  VkStructureType     sType;
  const void*         pNext;
  VkRenderPass        renderPass;
  VkFramebuffer       framebuffer;
  VkRect2D            renderArea;
  uint32_t            clearValueCount;
  const VkClearValue* pClearValues;
} VkRenderPassBeginInfo;

El siguiente paso es enlazar el pipeline a utilizar por medio de la función vkCmdBindPipeline(). El parámetro pipelineBindPoint indica el tipo de pipeline que estamos utilizando y puede tomar los valores VK_PIPELINE_BIND_POINT_COMPUTE o VK_PIPELINE_BIND_POINT_GRAPHICS.

void vkCmdBindPipeline (
  VkCommandBuffer     commandBuffer,
  VkPipelineBindPoint pipelineBindPoint,
  VkPipeline          pipeline);

A continuación hay que enlazar las entradas del pipeline. Para eso hay que utilizar las siguientes funciones: vkCmdBindVertexBuffers(), vkCmdBindIndexBuffer() y vkCmdBindDescriptorSets(). En este proyecto no se han incluido estas entradas en el pipeline.

Para lanzar el pipeline y realizar el dibujo se utilizan los comandos vkCmdDraw() o vkCmdDrawIndexed(). En el primer caso los vértices se leen secuencialmente. En el segundo caso se utiliza el buffer de índices para leer los vértices de forma indirecta.

void vkCmdDraw (
  VkCommandBuffer commandBuffer,
  uint32_t        vertexCount,
  uint32_t        instanceCount,
  uint32_t        firstVertex,
  uint32_t        firstInstance);

void vkCmdDrawIndexed (
  VkCommandBuffer commandBuffer,
  uint32_t        indexCount,
  uint32_t        instanceCount,
  uint32_t        firstIndex,
  int32_t         vertexOffset,
  uint32_t        firstInstance);

Para declarar el final de la descripción del renderpass se utiliza la función vkCmdEndRenderPass().

void vkCmdEndRenderPass (
  VkCommandBuffer commandBuffer);

Para finalizar la creación del buffer de comandos se utiliza la función vkEndCommandBuffer().

VkResult vkEndCommandBuffer (
  VkCommandBuffer commandBuffer);

El esquema de generación de un bloque de comandos es el siguiente:

Command Buffer

 

 

Modificaciones de la clase GERenderingContext

 

El método para rellenar los buffers de comandos se ha incluido en la clase GERenderingContext. La cabecera de la clase incorpora la declaración del método fillCommandBuffers().

//
// CLASE: GERenderingContext
//
// DESCRIPCIÓN: Clase que describe un contexto de renderizado
//
class GERenderingContext
{
  ...

public:
  GERenderingContext(GEGraphicsContext* gc, 
                     GEDrawingContext* dc, 
                     GEPipelineConfig* config);
  void destroy(GEGraphicsContext* gc);
  void fillCommandBuffers(std::vector<VkCommandBuffer> commandBuffers);
  
  ...
};

El contenido del método es el siguiente.

//
// FUNCIÓN: GERenderingContext::fillCommandBuffers()
//
// PROPÓSITO: Actualiza los buffers de comandos para añadir el renderizado
//
void GERenderingContext::fillCommandBuffers(
                         std::vector<VkCommandBuffer> commandBuffers)
{
  for (size_t i = 0; i < commandBuffers.size(); i++)
  {
    VkCommandBufferBeginInfo beginInfo = {};
    beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;

    if (vkBeginCommandBuffer(commandBuffers[i], &beginInfo) != VK_SUCCESS)
    {
      throw std::runtime_error("failed to begin recording command buffer!");
    }

    VkClearValue clearValues[2];
    clearValues[0].color = { 1.0f, 1.0f, 1.0f, 1.0f };
    clearValues[1].depthStencil = { 1.0f, 0 };

    VkRenderPassBeginInfo renderPassInfo = {};
    renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
    renderPassInfo.renderPass = renderPass;
    renderPassInfo.framebuffer = framebuffers[i];
    renderPassInfo.renderArea.offset = { 0, 0 };
    renderPassInfo.renderArea.extent = extent;
    renderPassInfo.clearValueCount = 2;
    renderPassInfo.pClearValues = clearValues;

    vkCmdBeginRenderPass(commandBuffers[i], &renderPassInfo, 
                                                     VK_SUBPASS_CONTENTS_INLINE);

    vkCmdBindPipeline(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, 
                                                               graphicsPipeline);

    vkCmdDraw(commandBuffers[i], 3, 1, 0, 0);

    vkCmdEndRenderPass(commandBuffers[i]);

    if (vkEndCommandBuffer(commandBuffers[i]) != VK_SUCCESS)
    {
      throw std::runtime_error("failed to record command buffer!");
    }
  }
}

 

 

Modificaciones de la clase GEApplication

 

La llamada al método fillCommandBuffers() se ha incluido tras la incialización de todos los componentes gráficos de la clase GEApplication. Al añadir los comandos la aplicación ya genera las imágenes, pero no modifica su tamaño al detectar cambios de tamaño en la ventana. Lo ideal es que las imágenes a generar tengan el mismo tamaño que la superficie de la ventana en la que se van a mostrar. De esta manera se optimiza la definición de las imágenes. Para añadir este efecto hay que reconstruir los componentes gráficos al detectar un cambio de tamaño, lo que se ha añadido al método resize().

//
// 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());
  GEPipelineConfig* config = getPipelineConfig(dc->getExtent());
  this->rc = new GERenderingContext(this->gc, this->dc, config);

  this->rc->fillCommandBuffers(this->cc->commandBuffers);

  mainLoop();

  cleanup();
}

//
// FUNCIÓN: GEApplication::resize()
//
// 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);
  }

  dc->recreate(gc, windowPos);
  rc->destroy(gc);
  cc->destroy(gc);
  delete rc;
  delete cc;

  GEPipelineConfig* config = getPipelineConfig(dc->getExtent());
  this->rc = new GERenderingContext(this->gc, this->dc, config);
  this->cc = new GECommandContext(this->gc, this->dc->getImageCount());
  this->rc->fillCommandBuffers(cc->commandBuffers);
}

 

 

Aspecto final

 

Con estos últimos cambios la ventana muestra por fin la imagen diseñada.

Captura 5