Escuela Técnica Superior de Ingeniería

 

Grado en Ingeniería Informática

Animación por Ordenador

Curso 2025/2026

 

Práctica 4b

Introducción de un Index Buffer

 

Objetivos

 

Modificar la aplicación gráfica para que los vértices se lean de forma indexada.

 

 

Las estructuras VkBuffer y VkDeviceMemory

 

La forma de crear un buffer de índices es idéntica a la forma en la que se crean los buffers de atributos de vértices. Es decir, hay que crear un VkBuffer, reservar memoria en un  VkDeviceMemory, mapear la memoria para almacenar los datos y vincular la memoria al buffer. La única diferencia es que al crear el buffer hay que indicar el valor VK_BUFFER_USAGE_INDEX_BUFFER_BIT en el campo usage de la estructura VkBufferCreateInfo.

 

 

Comandos

 

Para realizar un dibujo utilizando datos indexados se utiliza el comando vkCmdDrawIndexed(). El parámetro indexCount define el número de índices que se leerán para dibujar las primitivas. El parámetro firstIndex indica cual el el primer índice que se va a leer del buffer de índices (lo normal es que sea el 0). El parámetro vertexOffset es una constante que se suma a los valores leidos del buffer para obtener el índice de los vértices (generalmente este desplazamiento es 0). El parámetro instanceCount indica el número de instancias (es decir, el número de veces que se dibujarán las primitivas sobre el conjunto de vértices). El parámetro firstInstance es el valor que se asignará a la primera instancia. El Vertex Shader puede acceder al número de instancia que se está ejecutando por medio de la variable de entrada predefinida gl_InstanceIndex.

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

Para indicar cual es el buffer de índices a utilizar en el comando DrawIndexed se utiliza la función vkCmdBindIndexBuffer(). El parámetro indexType indica el tipo de dato que utiliza el buffer. Puede utilizarse el valor  VK_INDEX_TYPE_UINT16 para indicar que los índices se almacenan como “short” (enteros de 16 bits) o el valor VK_INDEX_TYPE_UINT32 para indicar que los índices se almacenan como "int" (enteros de 32 bits).

void vkCmdBindIndexBuffer(
  VkCommandBuffer commandBuffer,
  VkBuffer        buffer,
  VkDeviceSize    offset,
  VkIndexType     indexType);

 

 

La clase GEIndexBuffer

 

La clase GEIndexBuffer contine la información necesaria para construir un buffer de índices, es decir,  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 GEIndexBuffer
{
public:
  VkBuffer buffer;
  VkDeviceMemory memory;

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

Los métodos de la clase son los siguentes.

#include "GEIndexBuffer.h"

#include <iostream>

//
// FUNCIÓN: GEIndexBuffer::GEIndexBuffer()
//
// PROPÓSITO: Crea un Vertex Buffer
//
GEIndexBuffer::GEIndexBuffer(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_INDEX_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: GEIndexBuffer::destroy(GEGraphicsContext* gc)
//
// PROPÓSITO: Destruye los campos de un Index Buffer
//
void GEIndexBuffer::destroy(GEGraphicsContext* gc)
{
  vkDestroyBuffer(gc->device, buffer, nullptr);
  vkFreeMemory(gc->device, memory, nullptr);
}

 

 

Modificaciones de la clase GEFigure

 

La clase GEFigure se ha modificado para incluir el contenido de los índices (la constante indices) y las referencias al index buffer.

#pragma once

#include "GEGraphicsContext.h"
#include "GEVertex.h"
#include "GEVertexBuffer.h"
#include "GEIndexBuffer.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}}
};

const std::vector<uint16_t> indices =
{
  0,1,2
};

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

private:
  GEVertexBuffer* vbo;
  GEIndexBuffer* ibo;
};

El código de la clase modifica sus tres métodos. El constructor añade la creación del objeto GEIndexBuffer. El método destroy() añade la destrucción del buffer de índices. El método addCommands() añade el comando de selección del buffer de índices, vkCmdBindIndexBuffer, y sustituye el comando vkCmdDraw por el comando vkCmdDrawIndexed para indicar que se debe dibujar en modo indexado.

#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());

  size_t indexSize = sizeof(indices[0]) * indices.size();
  ibo = new GEIndexBuffer(gc, indexSize, indices.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;

  ibo->destroy(gc);
  delete ibo;
}

//
// 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);
  vkCmdBindIndexBuffer(commandBuffer, ibo->buffer, 0, VK_INDEX_TYPE_UINT16);
  vkCmdDrawIndexed(commandBuffer, (uint32_t)indices.size(), 1, 0, 0, 0);
}

 

 

Aspecto final

 

El aspecto final sigue siendo el mismo que en el proyecto anterior.

Captura