Escuela Técnica Superior de Ingeniería

 

Grado en Ingeniería Informática

Animación por Ordenador

Curso 2025/2026

 

Práctica 4a

Introducción de un Vertex Buffer

 

Objetivos

 

Modificar la aplicación para que los vértices del triángulo se lean de un buffer de memoria en lugar de incluirlos en el Vertex Shader.

 

 

La estructura VkPipelineVertexInputStateCreateInfo

 

Los puntos de entrada de la información en el pipeline de renderizado corresponden a las variables de entrada del Vertex Shader, que corresponden a los atributos de los vértices, y las variables uniformes definidas en cada shader. Para describir la forma de acceder a los atributos de los vértices se utiliza la estructura VkPipelineVertexInputStateCreateInfo que forma parte de la información necesaria para definir un pipeline gráfico.

typedef struct VkPipelineVertexInputStateCreateInfo {
  VkStructureType                          sType;
  const void*                              pNext;
  VkPipelineVertexInputStateCreateFlags    flags;
  uint32_t                                 vertexBindingDescriptionCount;
  const VkVertexInputBindingDescription*   pVertexBindingDescriptions;
  uint32_t                                 vertexAttributeDescriptionCount;
  const VkVertexInputAttributeDescription* pVertexAttributeDescriptions;
} VkPipelineVertexInputStateCreateInfo;

El campo sType es VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO.

Los campos pNext y flags están reservados para futuras ampliaciones y deben dejarse a nulo.

Los valores de los atributos de los vértices se almacenan en buffers, es decir, en bloques de memoria de la tarjeta gráfica. El campo vertexBindingDescriptionCount indica el número de buffers de los que se toman las entradas y el campo pVertexBindingDescriptions indica la forma de acceder a esos buffers.

El campo vertexAttributeDescriptionCount indica el número de atributos que se van a leer en el Vertex Shader y el campo pVertexAttributeDescriptions describe la forma de leer esos atributos a partir de los buffers.

A continuación se muestra la definición de la estructura VkVertexInputBindingDescription que describe la forma de acceder a un buffer de almacenamiento de valores de los atributos.

typedef struct VkVertexInputBindingDescription {
  uint32_t          binding;
  uint32_t          stride;
  VkVertexInputRate inputRate;
} VkVertexInputBindingDescription;

El campo binding se refiere al índice utilizado para referenciar al buffer. Por ejemplo, si los atributos se encuentran distribuidos entre tres buffers entonces es necesario definir tres estructuras con los bindings 0, 1 y 2.

El campo stride indica la cantidad de bytes que se leerán del buffer en cada ejecución del Vertex Shader. Por ejemplo, si el buffer almacena para cada vértice una estructura con un valor de tipo vec3 (12 bytes) y otro valor de tipo vec2 (8 bytes) el valor del campo stride sería 20.

El campo inputRate indica la frecuencia con la que se lee el buffer desde el Vertex Shader. Generalmente se utiliza el valor VK_VERTEX_INPUT_RATE_VERTEX, que indica que hay que leer los valores del buffer para cada vértice. También se puede usar el valor VK_VERTEX_INPUT_RATE_INSTANCE, que indica que se leerá del buffer para cada instancia de dibujo.

La descripción de los atributos se introduce mediante la estructura VkVertexInputAttributeDescription que se define a continuación.

typedef struct VkVertexInputAttributeDescription {
  uint32_t location;
  uint32_t binding;
  VkFormat format;
  uint32_t offset;
} VkVertexInputAttributeDescription;

El campo location indica el índice del atributo a describir. Este valor se puede fijar en el código del shader mediante el modificador layout. Por ejemplo, para fijar el índice 0 en un atributo se indicaría "layout(location = 0)" antes de la definición de la variable de entrada.

El campo binding indica el índice del buffer en el que se encuentra almacenado el valor del atributo.

El campo format indica el formato en el que está almacenado el valor del atributo en el buffer. Debe ser un valor de la enumeración VkFormat. Por ejemplo, un valor de tipo vec3 se almacena como un formato de tres valores en coma flotante de 32 bits lo que corresponde al valor VK_FORMAT_R32G32B32_SFLOAT.

El campo offset se refiere a la posición en la que comienza el dato en el bloque que se lee del buffer. Por ejemplo, si el buffer almacena una estructura con un vec3 y un vec2, el offset del primer atributo sería 0 y el offset del segundo atributo sería el tamaño del vec3 (12 bytes).

 

 

Las estructuras VkBuffer y VkDeviceMemory

 

Un buffer es una zona de memoria de la tarjeta gráfica en la que se almacenan datos a utilizar en el proceso de renderizado. Para crear un buffer se utiliza la función vkCreateBuffer().

VkResult vkCreateBuffer(
  VkDevice                     device,
  const VkBufferCreateInfo*    pCreateInfo,
  const VkAllocationCallbacks* pAllocator,
  VkBuffer*                    pBuffer);

La información necesaria para crear el buffer se introduce en la estructura VkBufferCreateInfo.

typedef struct VkBufferCreateInfo {
  VkStructureType     sType;
  const void*         pNext;
  VkBufferCreateFlags flags;
  VkDeviceSize        size;
  VkBufferUsageFlags  usage;
  VkSharingMode       sharingMode;
  uint32_t            queueFamilyIndexCount;
  const uint32_t*     pQueueFamilyIndices;
} VkBufferCreateInfo;

El campo sType debe ser VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO.

El campo pNext suele dejarse a nulo, aunque se han definido varias extensiones que permiten incuir información adicional.

El campo flags suele dejarse a nulo, pero pueden utilizarse bits para indicar si el buffer utiliza memoria protegida o puede tener asociada una memoria esparcida en el dispositivo.

El campo size se refiere al tamaño en bytes que requiere el buffer.

El campo usage indica el uso que tendrá el buffer, es decir, el tipo de buffer a crear. Para los buffers utilizados para almacenar atributos de vértices se utiliza el valor VK_BUFFER_USAGE_VERTEX_BUFFER_BIT. Otros valores comunes son VK_BUFFER_USAGE_INDEX_BUFFER_BIT (para los buffers de índices que utilizaremos en el proyecto 3.b) o VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT (para los buffers de variables uniformes vinculados a los descriptor sets que utilizaremos en el proyecto 3.c).

El campo sharingMode indica si el buffer va a ser compartido entre diferentes colas de comandos. Para indicar que el buffer no se va a compartir se utiliza el valor VK_SHARING_MODE_EXCLUSIVE. Si el buffer se va a compartir se utiliza el valor VK_SHARING_MODE_CONCURRENT. En ese caso hay que especificar cuales son las familias que pueden acceder al buffer de forma concurrente mediante los campos queueFamilyIndexCount (número de familias) y pQueueFamilyIndices (índices de las familias).

El objeto VkBuffer contiene la descripción del buffer, pero para ubicarlo en memoria es necesario crear una estructura paralela de tipo VkDeviceMemory, que describe la memoria ocupada por el buffer. Para alojar la memoria se utiliza la función vkAllocateMemory().

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

La estructura VkMemoryAllocateInfo describe el tamaño a reservar y el tipo de memoria de entre los diferentes tipos soportados por el dispositivo.

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

El campo sType debe ser

El campo pNext debe ser nulo.

El campo allocationSize contiene el tamaño en bytes de la memoria a alojar.

El campo memoryTypeIndex describe el tipo de memoria a utilizar. Se refiere al índice del tipo entre las propiedades de memoria del dispositivo. Para encontar ese índice hay que obtener las propiedades de memoria del dispositivo por medio de la función vkGetPhysicalDeviceMemoryProperties() y comparar los diferentes tipos con las propiedades necesarias para el buffer, que se pueden obtener con la función vkGetBufferMemoryRequirements().

Una vez reservada la memoria ya se pueden almacenar en ella los datos del buffer. La función vkMapMemory() obtiene un puntero a la memoria que se puede utilizar como referencia para copiar los datos. A partir del puntero se pueden volcar los datos con la función memcpy(). Es importante liberar el puntero con vkUnmapMemory() porque mientras la memoria está mapeada, la GPU no puede acceder a ella.

VkResult vkMapMemory(
  VkDevice         device,
  VkDeviceMemory   memory,
  VkDeviceSize     offset,
  VkDeviceSize     size,
  VkMemoryMapFlags flags,
  void**           ppData);
  
void vkUnmapMemory(
  VkDevice         device,
  VkDeviceMemory   memory);

Los parámetros device y memory se refieren al dispositivo y a la memoria a mapear. El parámetro offset indica el punto en el que se comenzará a mapear la memoria. El parámetro size es el tamaño en bytes a mapear. Se puede utilizar el valor VK_WHOLE_SIZE para mapear hasta el final de la memoria reservada. El parámetro flags está reservado para un uso futuro. El puntero a la memoria se devuelve en el parámetro ppData.

Una vez creado el buffer, reservada la memoria y copiados en ella los datos, es necesario vincular el buffer a la memoria por medio de la función vkBindBufferMemory().

VkResult vkBindBufferMemory(
  VkDevice       device,
  VkBuffer       buffer,
  VkDeviceMemory memory,
  VkDeviceSize   memoryOffset);

 

 

Comandos

 

El paso final para utilizar los buffers como puntos de entrada del proceso de renderizado es activarlos en el buffer de comandos antes de llamar al comando Draw. Para eso se utiliza la función vkCmdBindVertexBuffers().

void vkCmdBindVertexBuffers(
  VkCommandBuffer     commandBuffer,
  uint32_t            firstBinding,
  uint32_t            bindingCount,
  const VkBuffer*     pBuffers,
  const VkDeviceSize* pOffsets);

Como los atributos pueden estar almacenados en varios buffers, el parámetro pBuffers contiene una lista de buffers. El parámetro firstBinding indica cual es el primer binding a vincular. El parámetro bindingCount indica el número de bindings a vincular con los buffers. El parámetro pOffsets define el desplazamiento inicial sobre cada buffer, es decir, el byte en el que se comenzará a leer cada buffer.

En una escena a dibujar generalmente aparecen varios objetos cuyos vértices estarán almacenados en buffers diferentes. A la hora de hacer el dibujo completo hay que dibujar los objetos secuencialmente. Para dibujar cada objeto hay que enlazar sus VertexBuffers y seguidamente ejecutar el comando Draw.

 

 

Shaders

 

Para incluir los atributos de los vértices solo es necesario modificar el Vertex Shader dejando inalterado el Fragment Shader. En este caso se va a sustituir la declaración de los atributos en el mismo código con la declaración de los atributos como variables de entrada (in). Para asegurar la posición asociada a cada atributo se ha utilizado el modificador layout con la propiedad location. De esta forma se garantiza que el atributo inPosition corresponde a la posición 0 y que el atributo inColor corresponde a la posición 1. El código de la función main() se ha modificado para tomar el valor de esas variables de entrada.

#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec3 inColor;

layout(location = 0) out vec3 fragColor;

void main() 
{
  gl_Position = vec4(inPosition, 0.0, 1.0);
  fragColor = inColor;
}

 

 

La estructura GEVertex

 

Para describir la información asociada a cada vértice se ha incluido una nueva estructura llamada GEVertex. Esta estructura corresponde al bloque de información asociada a cada vértice que se almacenará en el buffer. En cada ejecución del Vertex Shader se leerá de la memoria un bloque de este tipo y se asignará a las variables de entrada del Vertex Shader. En este caso la estructura está formada por dos campos que almacenarán los valores de los atributos inPosition e inColor.

#pragma once

#include <glm\glm.hpp>

typedef struct 
{
  glm::vec2 pos;
  glm::vec3 color;
} GEVertex;

 

 

La clase GEVertexBuffer

 

La clase GEVertexBuffer contine la información necesaria para construir y gestionar un buffer con los valores de los atributos de los vértices. Como hemos visto, esto requiere construir un objeto VkBuffer para describir la estructura del buffer y un objeto VkDeviceMemory para definir la zona de memoria donde almacenar la información del buffer.

#pragma once

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

class GEVertexBuffer
{
public:
  VkBuffer buffer;
  VkDeviceMemory memory;

  GEVertexBuffer(GEGraphicsContext* gc, size_t size, const void* data);
  void destroy(GEGraphicsContext* gc);
};

A continuación se muestra el contenido de los métodos de laa clase GEVertexBuffer.

#include "GEVertexBuffer.h"

#include <iostream>

//
// FUNCIÓN: GEVertexBuffer::GEVertexBuffer()
//
// PROPÓSITO: Crea un Vertex Buffer
//
GEVertexBuffer::GEVertexBuffer(GEGraphicsContext* gc, size_t size, const void* data)
{
  VkBufferCreateInfo bufferInfo = {};
  bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
  bufferInfo.size = size;
  bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
  bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

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

  VkMemoryRequirements memRequirements;
  vkGetBufferMemoryRequirements(gc->device, buffer, &memRequirements);

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

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

  vkBindBufferMemory(gc->device, buffer, memory, 0);

  void* gpudata;
  vkMapMemory(gc->device, memory, 0, size, 0, &gpudata);
  memcpy(gpudata, data, size);
  vkUnmapMemory(gc->device, memory);
}

//
// FUNCIÓN: GEVertexBuffer::destroy(GEGraphicsContext* gc)
//
// PROPÓSITO: Destruye los campos de un Vertex Buffer
//
void GEVertexBuffer::destroy(GEGraphicsContext* gc)
{
  vkDestroyBuffer(gc->device, buffer, nullptr);
  vkFreeMemory(gc->device, memory, nullptr);
}

 

 

La clase GEFigure

 

La clase GEFigure contiene la información y las funciones necesarias para representar la figura a dibujar, en este caso el triángulo. Los campos de la clase incluyen las referencias al objeto GEVertexBuffer en el que se almacenan los atributos de los vértices. Los métodos de la clase permiten inicializar y finalizar el uso de la figura, es decir, crear y destruir los buffers que la forman, y añadir los comandos de dibujo a un determinado buffer de comandos. La constante vertices contiene los datos que se almacenarán en el vertex buffer.

#pragma once

#include "GEGraphicsContext.h"
#include "GEVertex.h"
#include "GEVertexBuffer.h"
#include <vector>

const std::vector<GEVertex> vertices =
{
{{0.0f, -0.5f}, {1.0f, 0.0f, 0.0f}},
{{0.5f, 0.5f}, {0.0f, 1.0f, 0.0f}},
{{-0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}}
};

class GEFigure
{
public:
  GEFigure(GEGraphicsContext* gc);
  void destroy(GEGraphicsContext* gc);
  void addCommands(VkCommandBuffer commandBuffer, int index);

private:
  GEVertexBuffer* vbo;
};

A continuación se muestra el código de la clase. El constructor es el encargado de crear el vertex buffer. Para ello utiliza los datos de la constante vertices incluida en el fichero de cabecera de la clase. El método destroy() destruye el vertex buffer. El método addCommands() recibe un buffer de comandos y le añade los comandos vkCmdBindVertexBuffers y vkCmdDraw para lanzar el proceso de dibujo de la figura.

#include "GEFigure.h"

#include "GEVertex.h"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <iostream>

//
// FUNCIÓN: GEFigure::GEFigure(GEGraphicsContext* gc)
//
// PROPÓSITO: Crea el Vertex Buffer
//
GEFigure::GEFigure(GEGraphicsContext* gc)
{
  size_t vertexSize = sizeof(GEVertex) * vertices.size();
  vbo = new GEVertexBuffer(gc, vertexSize, vertices.data());
}

//
// FUNCIÓN: GEFigure::destroy(GEGraphicsContext* gc)
//
// PROPÓSITO: Libera los buffers de la figura
//
void GEFigure::destroy(GEGraphicsContext* gc)
{
  vbo->destroy(gc);
  delete vbo;
}

//
// FUNCIÓN: CAFigure::addCommands(VkCommandBuffer commandBuffer, int index)
//
// PROPÓSITO: Añade los comandos de renderizado al command buffer
//
void GEFigure::addCommands(VkCommandBuffer commandBuffer, int index)
{
  VkDeviceSize offset = 0;
  vkCmdBindVertexBuffers(commandBuffer, 0, 1, &(vbo->buffer), &offset);
  vkCmdDraw(commandBuffer, (uint32_t) vertices.size(), 1, 0, 0);
}

 

 

La clase GEScene

 

La clase GEScene se ha añadido al proyecto para almacenar las referencias a las figuras que forman parte del modelo 3D a representar. Esta clase contiene el objeto GERenderingContext con la configuración del proceso de renderizado (renderpass, pipeline y framebuffers), que se ha trasladado desde la versión anterior de la clase GEApplication, así como el campo figure que contiene la descripción del triángulo que vamos a dibujar. 

#pragma once

#include "GEGraphicsContext.h"
#include "GEDrawingContext.h"
#include "GECommandContext.h"
#include "GERenderingContext.h"

#include "GEFigure.h"
#include <vulkan/vulkan.h>
#include <vector>

//
// CLASE: GEScene
//
// DESCRIPCIÓN: Clase que describe una escena
//
class GEScene
{
private:
  GERenderingContext* rc;
  GEFigure* figure;

public:
  GEScene(GEGraphicsContext* gc, GEDrawingContext* dc, GECommandContext* cc);
  void destroy(GEGraphicsContext* gc);
  void recreate(GEGraphicsContext* gc, GEDrawingContext* dc, GECommandContext* cc);

private:
  void fillCommandBuffers(std::vector commandBuffers);
  GEPipelineConfig* createPipelineConfig(VkExtent2D extent);
};

Los métodos de la clase son los siguientes:

  • GEScene(): Constructor de la clase. Es el responsable de crear el conterxto de renderizado, crear la figura que contiene la escena y rellenar los buffers de comandos.

  • destroy(): Destruye los componentes de la clase.

  • recreate(): Reconstruye los componentes ante un cambio de tamaño de la ventana dela aplicación.

  • fillCommandBuffers(): Rellena los buffers de comandos.

  • createPipelineConfig(): Crea la configuración del pipeline de renderizado. En este caso se ha modificado la descripción de los atributos de los vértices para indicar que se van a utilizar dos atributos (pos y color) de tipo vec2 y vec3 respectivamente.

El código de los métodos es el siguiente.

#include "GEScene.h"

#include <windows.h>
#include "resource.h"

//
// FUNCIÓN: GEScene::GEScene(GEGraphicsContext* gc, GEDrawingContext* dc)
//
// PROPÓSITO: Crea la escena
//
GEScene::GEScene(GEGraphicsContext* gc, GEDrawingContext* dc, GECommandContext* cc)
{
  GEPipelineConfig* config = createPipelineConfig(dc->getExtent());
  this->rc = new GERenderingContext(gc, dc, config);

  this->figure = new GEFigure(gc);

  fillCommandBuffers(cc->commandBuffers);
}

//
// FUNCIÓN: GEScene::destroy(GEGraphicsContext* gc)
//
// PROPÓSITO: Reconstruye los componentes gráficos de la escena
//
void GEScene::destroy(GEGraphicsContext* gc)
{
  rc->destroy(gc);
  figure->destroy(gc);

  delete rc;
  delete figure;
}

//
// FUNCIÓN: GEScene::recreate(GEGraphicsContext* gc, GEDrawingContext* dc)
//
// PROPÓSITO: Reconstruye los componentes gráficos de la escena
//
void GEScene::recreate(GEGraphicsContext* gc, 
                       GEDrawingContext* dc, 
                       GECommandContext* cc)
{
  rc->destroy(gc);
  GEPipelineConfig* config = createPipelineConfig(dc->getExtent());
  this->rc = new GERenderingContext(gc, dc, config);
  fillCommandBuffers(cc->commandBuffers);
}

//
// FUNCIÓN: CAScene::addCommands(VkCommandBuffer commandBuffer, int index)
//
// PROPÓSITO: Añade los comandos de renderizado al command buffer
//
void GEScene::addCommands(VkCommandBuffer commandBuffer, int index)
{
  figure->addCommands(commandBuffer, index);
}

//
// FUNCIÓN: GEScene::createPipelineConfig()
//
// PROPÓSITO: Obtiene la configuración del pipeline de renderizado
//
GEPipelineConfig* GEScene::createPipelineConfig(VkExtent2D extent)
{
  GEPipelineConfig* config = new GEPipelineConfig();
  config->vertex_shader = IDR_HTML1;
  config->fragment_shader = IDR_HTML2;

  config->attrStride = sizeof(GEVertex);
  config->attrOffsets.resize(2);
  config->attrOffsets[0] = offsetof(GEVertex, pos);
  config->attrOffsets[1] = offsetof(GEVertex, color);
  config->attrFormats.resize(2);
  config->attrFormats[0] = VK_FORMAT_R32G32_SFLOAT;
  config->attrFormats[1] = VK_FORMAT_R32G32B32_SFLOAT;

  config->descriptorTypes.resize(0);
  config->descriptorStages.resize(0);

  config->depthTestEnable = VK_TRUE;
  config->cullMode = VK_CULL_MODE_NONE;
  config->extent = extent;

  return config;
}

 

 

Modificaciones de las clases GEApplication y GERenderingContext

 

Para finalizar el proyecto se han realizado pequeñas modificaciones en las clases GEApplication y GERenderingContext. En la clase GEApplication se ha eliminado la referencia al objeto GERenderingContext y se ha añadido la referencia al objeto GEScene. También se ha eliminado el método createPipelineConfig() ya que la generación del contexto de renderizado se ha trasladado a la clase GEScene.

//
// 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->scene = new GEScene(gc, dc, cc);

  mainLoop();

  cleanup();
}

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

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

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

  this->cc = new GECommandContext(this->gc, this->dc->getImageCount());

  scene->recreate(gc, dc, cc);
}	

Respecto a la clase GERenderingContext el cambio ha consistido en modificar el método fillCommandBuffers() para dividirlo en dos métodos: startFillingCommandBuffers() y endFillingCommandBuffers().

//
// FUNCIÓN: GERenderingContext::startFillingCommandBuffers()
//
// PROPÓSITO: Actualiza los buffers de comandos para añadir el renderizado
//
void GERenderingContext::startFillingCommandBuffers(
                                  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);

    scene->addCommands(commandBuffers[i], i);

    vkCmdEndRenderPass(commandBuffers[i]);

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

//
// FUNCIÓN: GERenderingContext::endFillingCommandBuffers()
//
// PROPÓSITO: Actualiza los buffers de comandos para añadir el renderizado
//
void GERenderingContext::endFillingCommandBuffers(
                                     std::vector<VkCommandBuffer> commandBuffers)
{
  for (size_t i = 0; i < commandBuffers.size(); i++)
  {
    vkCmdEndRenderPass(commandBuffers[i]);

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

 

 

Aspecto final

 

El aspecto final es el mismo que el del proyecto Project3e ya que solo se ha modificado la forma en la que se leen los valores de los atributos del triángulo.

Captura