Escuela Técnica Superior de Ingeniería

 

Grado en Ingeniería Informática

Animación por Ordenador

Curso 2025/2026

 

Práctica 4c

Introducción de conjuntos de descriptores

 

Objetivos

 

Modificar el proyecto para generar el movimiento del triángulo. Incluir variables uniformes para parametrizar la posición y el tamaño del triángulo. Crear conjuntos de descriptores para asignar el valor de las variables uniformes.

 

 

La estructura VkPipelineLayoutCreateInfo

 

La estructura VkGraphicsPipelineCreateInfo utilizada para crear el pipeline completo contiene un campo denominado layout que describe la plantilla de lectura de datos de los diferentes shaders. Este campo layout es un objeto de tipo VkPipelineLayout. Para crear este objeto se utiliza la función vkCreatePipelineLayout().

VkResult vkCreatePipelineLayout(
  VkDevice                          device,
  const VkPipelineLayoutCreateInfo* pCreateInfo,
  const VkAllocationCallbacks*      pAllocator,
  VkPipelineLayout*                 pPipelineLayout);

La información necesaria para crear este objeto se describe mediante una estructura VkPipelineLayoutCreateInfo.

typedef struct VkPipelineLayoutCreateInfo {
    VkStructureType                 sType;
    const void*                     pNext;
    VkPipelineLayoutCreateFlags     flags;
    uint32_t                        setLayoutCount;
    const VkDescriptorSetLayout*    pSetLayouts;
    uint32_t                        pushConstantRangeCount;
    const VkPushConstantRange*      pPushConstantRanges;
} VkPipelineLayoutCreateInfo;

El campo sType debe tener el valor VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO.

Los campos pNext y flags están reservados para uso futuro y deben dejarse a cero.

El campo pSetLayouts define las plantillas de los conjuntos de descriptores y el campo setLayoutCount indica cuantas plantillas de conjuntos de descriptores se están definiendo.

El campo pPushConstantRanges define las push constants a utilizar en el pipeline y el campo pushConstantRangeCount indica cuantas push constants se están definiendo. Las push constants son constantes cuyo valor se puede asignar directamente mediante comandos sin necesidad de estar almacenadas en buffers de la tarjeta gráfica. Suponen una forma más rápida de asignar valores a variables uniformes de los shaders, pero tienen restricciones en cuanto al tipo de variable (no se admiten texturas, por ejemplo) y al número de variables que se pueden asignar. En esta asignatura no se van a utilizar este tipo de constantes.

Las plantillas de conjuntos de descriptores son objetos de tipo VkDescriptorSetLayout que se crean por medio de la función vkCreateDescriptorSetLayout().

VkResult vkCreateDescriptorSetLayout(
  VkDevice                               device,
  const VkDescriptorSetLayoutCreateInfo* pCreateInfo,
  const VkAllocationCallbacks*           pAllocator,
  VkDescriptorSetLayout*                 pSetLayout);

La información necesaria para crear estos objetos se describe en la estuctura VkDescriptorSetLayoutCreateInfo.

typedef struct VkDescriptorSetLayoutCreateInfo {
  VkStructureType                     sType;
  const void*                         pNext;
  VkDescriptorSetLayoutCreateFlags    flags;
  uint32_t                            bindingCount;
  const VkDescriptorSetLayoutBinding* pBindings;
} VkDescriptorSetLayoutCreateInfo;

El campo sType debe tener el valor VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO.

Los campos pNext y flags suelen dejarse a cero.

Las variables uniformes que se asignan por medio de conjuntos de descriptores se agrupan en bloques que comparten un índice denominado binding. Cada grupo será leído de un buffer de la tarjeta gráfica. El campo bindingCount indica cuantos grupos se declaran en la plantilla y el campo pBindings contiene su descripción.

typedef struct VkDescriptorSetLayoutBinding {
  uint32_t           binding;
  VkDescriptorType   descriptorType;
  uint32_t           descriptorCount;
  VkShaderStageFlags stageFlags;
  const VkSampler*   pImmutableSamplers;
} VkDescriptorSetLayoutBinding;

El campo binding indica el índice asignado al grupo de descriptores.

El campo descriptorType indica la forma en la que está almacenada la información del conjunto de descriptores. Para indicar que se leerá de un buffer uniforme se utiliza el valor VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER. Las variables uniformes utilizadas para almacenar texturas se declaran como VK_DESCRIPTOR_TYPE_SAMPLER. Existen bastantes tipos de descriptores aunque estos dos son los más comunes.

El campo descriptorCount indica el tamaño en bytes que ocupa el grupo, es decir, la cantidad de memoria que hay que leer del buffer para obtener los datos.

El campo stageFlags indica cuales son los shaders que van a utilizar el grupo de descriptores.

El campo pImmutableSamplers solo se utiliza para descriptores de tipo sampler para indicar como inicializar estos descriptores y que una vez inicializados no podrán ser actualizados.

 

 

La estructura VkDescriptorSet

 

Al definir el pipeline de renderizado se describe la plantilla o interfaz de los conjuntos de descriptores que se van a utilizar, pero no se definen los conjuntos de descriptores propiamente. Para crear conjuntos de descriptores se necesita crear en primer lugar un generador de conjuntos de descriptores. Este generador es un objeto VkDescriptorPool que se crea mediante la función vkCreateDescriptorPool().

VkResult vkCreateDescriptorPool(
  VkDevice                          device,
  const VkDescriptorPoolCreateInfo* pCreateInfo,
  const VkAllocationCallbacks*      pAllocator,
  VkDescriptorPool*                 pDescriptorPool);

La información necesaria para crear el objeto se incluye en una estructura VkDescriptorPoolCreateInfo.

typedef struct VkDescriptorPoolCreateInfo {
  VkStructureType             sType;
  const void*                 pNext;
  VkDescriptorPoolCreateFlags flags;
  uint32_t                    maxSets;
  uint32_t                    poolSizeCount;
  const VkDescriptorPoolSize* pPoolSizes;
} VkDescriptorPoolCreateInfo;

El campo sType debe ser VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO.

El campo pNext está reservado para un uso futuro y debe ser nulo.

El campo flags permite indicar algunos detalles sobre la forma de operar en los descriptores, pero suele dejarse a nulo.

El campo maxSets indica el máximo número de conjuntos de descriptores que podran generarse mediante el generador.

El campo pPoolSizes indica el tamaño de los conjuntos de descriptores a crear. Hay que incluir un objeto VkDescriptorPoolSize por cada grupo de descriptores que queramos considerar en el pipeline, es decir, por cada binding.

El campo poolSizeCount indica cuantos objetos VkDescriptorPoolSize contiene el array pPoolSizes.

typedef struct VkDescriptorPoolSize {
  VkDescriptorType type;
  uint32_t         descriptorCount;
} VkDescriptorPoolSize;

EL campo type de la estructura VkDescriptorPoolSize indica el tipo de conjunto de descriptor a crear (VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER o VK_DESCRIPTOR_TYPE_SAMPLER).

El campo descriptorCount indica el número de conjuntos de descriptores a crear. Si hay que modificar el contenido del conjunto de descriptores cada vez que se genere la imagen este número tiene que ser el número de imágenes del swap chain.

Una vez creado el objeto VkDescriptorPool se pueden generar los objetos VkDescriptorSet que definen los conjuntos de descriptores. Para esto se utiliza la función vkAllocateDescriptorSets().

VkResult vkAllocateDescriptorSets(
  VkDevice                           device,
  const VkDescriptorSetAllocateInfo* pAllocateInfo,
  VkDescriptorSet*                   pDescriptorSets);

La información para crear los objetos VkDescriptorSet es la siguiente:

typedef struct VkDescriptorSetAllocateInfo {
  VkStructureType              sType;
  const void*                  pNext;
  VkDescriptorPool             descriptorPool;
  uint32_t                     descriptorSetCount;
  const VkDescriptorSetLayout* pSetLayouts;
} VkDescriptorSetAllocateInfo;

El campo sType debe ser VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO.

El campo pNext debe ser nulo y está reservado para un uso futuro.

El campo descriptorPool contiene el generador de conjuntos de descriptores utilizado en la función.

El campo descriptorSetCount indica el número de conjuntos de descriptores a generar.

El campo pSetLayouts es un array con las descripciones de los conjuntos de descriptores.

Para definir finalmente cual es el buffer de memoria del que deben leerse los datos asociados a los conjuntos de descriptores se utiliza la función vkUpdateDescriptorSets().

void vkUpdateDescriptorSets(
  VkDevice                    device,
  uint32_t                    descriptorWriteCount,
  const VkWriteDescriptorSet* pDescriptorWrites,
  uint32_t                    descriptorCopyCount,
  const VkCopyDescriptorSet*  pDescriptorCopies);

Esta función permirte actualizar los conjuntos de descriptores de dos formas. La opción write escribe el valor de los descriptores leyendo de un buffer. La opción copy permite copiar valores entre unos descriptores y otros. Si se especifican las dos formas, en primer lugar se leen los datos de los buffers y a continuación se realizarían las copias. En estas prácticas solo vamos a utilizar la opción write.

Para describir la forma de leer los datos de los conjuntos de descriptores a partir de un buffer almacenado en la memoria del dispositivo se utiliza una estructura  VkWriteDescriptorSet.

typedef struct VkWriteDescriptorSet {
  VkStructureType               sType;
  const void*                   pNext;
  VkDescriptorSet               dstSet;
  uint32_t                      dstBinding;
  uint32_t                      dstArrayElement;
  uint32_t                      descriptorCount;
  VkDescriptorType              descriptorType;
  const VkDescriptorImageInfo*  pImageInfo;
  const VkDescriptorBufferInfo* pBufferInfo;
  const VkBufferView*           pTexelBufferView;
} VkWriteDescriptorSet;

El campo sType debe ser VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET.

El campo pNext debe ser nulo.

El campo dstSet se refiere al conjunto de descriptores a rellenar.

El campo dstBinding se refiere al índice utilizado para referenciar el conjunto de descriptores en los shaders.

El campo descriptorType indica el tipo de almacenamiento y determina el sentido del resto de los campos. Si el tipo es VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER la descripción del buffer se incluye en el campo pBufferInfo. Si el tipo es  VK_DESCRIPTOR_TYPE_SAMPLER la descripción de la imagen se incluye en el campo pImageInfo. Si el tipo es VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER la descripción se incluye en el campo pTexelBufferView.

El campo descriptorCount suele indicar el tamaño en bytes a leer, aunque depende cual sea el tipo de descriptor.

La estructura VkDescriptorBufferInfo identifica el buffer del que se leerán los datos, el desplazamiento inicial (suele ser 0) y el número de bytes a leer.

typedef struct VkDescriptorBufferInfo {
  VkBuffer     buffer;
  VkDeviceSize offset;
  VkDeviceSize range;
} VkDescriptorBufferInfo;

 

 

Comandos

 

Cuando un pipeline utiliza variables uniformes almacenadas en conjuntos de descriptores es necesario asignar estos conjuntos de descriptores antes de lanzar un comando Draw. Para asignar estos descriptores se utiliza el comando vkCmdBindDescriptorSets().

void vkCmdBindDescriptorSets(
  VkCommandBuffer        commandBuffer,
  VkPipelineBindPoint    pipelineBindPoint,
  VkPipelineLayout       layout,
  uint32_t               firstSet,
  uint32_t               descriptorSetCount,
  const VkDescriptorSet* pDescriptorSets,
  uint32_t               dynamicOffsetCount,
  const uint32_t*        pDynamicOffsets);

El parámetro commandBuffer indica el buffer al que se añade el comando.

El parámetro pipelineBindPoint indica el tipo de pipeline (VK_PIPELINE_BIND_POINT_GRAPHICS).

El parámetro layout contiene la plantilla que se utilizó para definir los conjuntos de descriptores al crear el pipeline.

El parámetro firstSet indica el primer índice de los bindings a asignar.

El parámetro descriptorSetCount indica cuantos conjuntos de descriptores se van a asignar consecutivamente en el comando.

El parámetro pDescriptorSets contiene la definición de los conjuntos de descriptores.

Los parámetros dynamicOffsetCount y pDynamicOffsets permiten añadir desplazamientos a las posiciones en las que se leerán los buffers asociados a los conjuntos de descriptores. Suelen dejarse a cero.

 

 

Shaders

 

Para parametrizar la posición y el tamaño del triángulo solo es necesario modificar el Vertex Shader dejando inalterado el Fragment Shader. Para ello se incluye una variable denominada ubo formada por una estructura con tres campos: posX, que indica la posición del centro del triángulo sobre el eje X; posY, que indica la posición del centro del triángulo sobre el eje Y; y size, que indica el tamaño del triángulo. Esta variable uniforme se vinculará a un descriptor set al que se le asigna el índice 0 por medio del modificador layout. El cálculo de la variable de salida gl_Position se ha modificado para vincularlo a la posición del centro y al tamaño indicado en la variable uniforme ubo.

#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(binding = 0) uniform UniformBufferObject {
  float posX;
  float posY;
  float size;
} ubo;

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

layout(location = 0) out vec3 fragColor;

void main() 
{
  gl_Position = vec4(ubo.size*inPosition.x+ubo.posX, 
                     ubo.size*inPosition.y+ubo.posY, 0.0, 1.0);
  fragColor = inColor;
}

 

 

La estructura GETransform

 

Para describir la información asociada al bloque de variables uniformes se ha incluido una nueva estructura llamada GETransform. Esta estructura corresponde al bloque de información que se almacenará en el buffer vinculado a la variable uniforme ubo introducida en el Vertex Shader.

#pragma once

//
// ESTRUCTURA: GETransform
//
// DESCRIPCIÓN: Estructura que almacena los datos necesarios para desarrollar el 
//              movimiento del triángulo
//
typedef struct 
{
  float posX;
  float posY;
  float size;
} GETransform;

 

 

La clase GEUniformBuffer

 

La clase GEUniformBuffer contiene los objetos necesarios para almacenar los valores de las variables uniformes. Esto requiere un objeto VkBuffer para definir el buffer y un objeto VkDeviceMemory para definir la memoria utilizada para almacenarlo. En el caso de las variables uniformes hay que tener en cuenta que sus valores van a ser modificados en cada imagen. Esto requiere que haya una versión diferente de los buffers para cada imagen de la swapchain. Por esta razón la clase GEUniformBuffer contiene varias versiones de los objetos VkBuffer y VkDeviceMemory almacenadas en vectores y un método update() para almacenar los valores de las variables uniformes en uno de los buffers.

#pragma once

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

//
// CLASE: GEUniformBuffer
//
// DESCRIPCIÓN: Clase que describe un conjunto de buffers para almacenar variables
//              uniformes 
//
class GEUniformBuffer
{
public:
  size_t bufferSize;
  std::vector<VkBuffer> buffers;
  std::vector<VkDeviceMemory> memories;

  GEUniformBuffer(GEGraphicsContext* gc, uint32_t imageCount, size_t bufferSize);
  void update(GEGraphicsContext* gc, uint32_t currentImage, size_t size, 
                                                            const void* data);
  void destroy(GEGraphicsContext* gc);
};

A continuación se muestra el código de los métodos de la clase.

#include "GEUniformBuffer.h"

#include <iostream>

//
// FUNCIÓN: GEUniformBuffer::GEUniformBuffer()
//
// PROPÓSITO: Crea una lista de Uniform Buffers asociados a cada imagen a generar
//
GEUniformBuffer::GEUniformBuffer(GEGraphicsContext* gc, uint32_t imageCount, 
                                                                size_t bufferSize)
{
  this->bufferSize = bufferSize;
  this->buffers.resize(imageCount);
  this->memories.resize(imageCount);

  for (uint32_t i = 0; i < imageCount; i++)
  {
    VkBuffer buffer;
    VkDeviceMemory deviceMemory;

    VkBufferCreateInfo bufferInfo = {};
    bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
    bufferInfo.size = bufferSize;
    bufferInfo.usage = VK_BUFFER_USAGE_UNIFORM_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, &deviceMemory)
                                                                    != VK_SUCCESS)
    {
      throw std::runtime_error("failed to allocate buffer memory!");
    }

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

    buffers[i] = buffer;
    memories[i] = deviceMemory;
  }
}

//
// FUNCIÓN: GEUniformBuffer::update()
//
// PROPÓSITO: Actualiza el valor almacenado en un Uniform Buffer
//
void GEUniformBuffer::update(GEGraphicsContext* gc, uint32_t currentImage, 
                                                    size_t size, const void* data)
{
  void* mdata;
  vkMapMemory(gc->device, memories[currentImage], 0, size, 0, &mdata);
  memcpy(mdata, data, size);
  vkUnmapMemory(gc->device, memories[currentImage]);
}

//
// FUNCIÓN: GEUniformBuffer::destroy(GEGraphicsContext* gc)
//
// PROPÓSITO: Destruye los campos de un Uniform Buffer
//
void GEUniformBuffer::destroy(GEGraphicsContext* gc)
{
  uint32_t size = (uint32_t) buffers.size();
  for (uint32_t i = 0; i < size; i++)
  {
    vkDestroyBuffer(gc->device, buffers[i], nullptr);
    vkFreeMemory(gc->device, memories[i], nullptr);
  }
}

 

 

La clase GEDescriptorSet

 

La clase GEDescriptorSet contiene la definición de un conjunto de descriptores. Cada descriptor corresponde a una variable uniforme que estará almacenada en un GEUniformBuffer. (En realidad un descriptor puede ser de varios tipos, como variables uniformes, texturas, almacenes, ...).  La clase está formada por el generador de conjuntos de descriptores (VkDescriptorPool) y un vector de conjuntos de descriptores (VkDescriptorSet), cada uno asociada a una de las imágenes de la swapchain. La estructura del conjunto de descriptores debe coincidir con la plantilla de conjuntos de descriptores utilizada en el pipeline.

#pragma once

#include <vulkan/vulkan.h>
#include <vector>
#include "GEGraphicsContext.h"
#include "GERenderingContext.h"
#include "GEUniformBuffer.h"

//
// CLASE: GEDescriptorSet
//
// DESCRIPCIÓN: Clase que describe un conjunto de descriptores 
//
class GEDescriptorSet
{
private:
  VkDescriptorPool descriptorPool;

public:
  std::vector<VkDescriptorSet> descriptorSets;

public:
  GEDescriptorSet(GEGraphicsContext* gc, GERenderingContext* rc, 
                                         std::vector<GEUniformBuffer*> ubos);
  void destroy(GEGraphicsContext* gc);
};

A continuación se muestra el código de los métodos de la clase.

#include "GEDescriptorSet.h"

#include "GEUniformBuffer.h"
#include <iostream>

//
// FUNCIÓN:GEDescriptorSet::GEDescriptorSet()
// 
// PROPÓSITO: Crea los conjuntos de descriptores asociados a los buffers
//
GEDescriptorSet::GEDescriptorSet(GEGraphicsContext* gc, 
                                 GERenderingContext* rc, 
                                 std::vector<GEUniformBuffer*> ubos)
{
  uint32_t bufferCount = (uint32_t) ubos.size();
  uint32_t imageCount = rc->imageCount;

  std::vector<VkDescriptorPoolSize> poolSizes(bufferCount);

  for (uint32_t i = 0; i < bufferCount; i++)
  {
    VkDescriptorPoolSize poolSize = {};
    poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
    poolSize.descriptorCount = imageCount;
    poolSizes[i] = poolSize;
  }

  VkDescriptorPoolCreateInfo poolInfo = {};
  poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
  poolInfo.poolSizeCount = (uint32_t)poolSizes.size();
  poolInfo.pPoolSizes = poolSizes.data();
  poolInfo.maxSets = imageCount;

  if (vkCreateDescriptorPool(gc->device, &poolInfo, nullptr, &this->descriptorPool)
                                                                     != VK_SUCCESS)
  {
    throw std::runtime_error("failed to create descriptor pool!");
  }

  std::vector<VkDescriptorSetLayout> layouts(imageCount, rc->descriptorSetLayout);
  VkDescriptorSetAllocateInfo allocInfo = {};
  allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
  allocInfo.descriptorPool = this->descriptorPool;
  allocInfo.descriptorSetCount = imageCount;
  allocInfo.pSetLayouts = layouts.data();

  descriptorSets.resize(imageCount);

  if (vkAllocateDescriptorSets(gc->device, &allocInfo, descriptorSets.data()) 
                                                                     != VK_SUCCESS)
  {
    throw std::runtime_error("failed to allocate descriptor sets!");
  }

  for (size_t i = 0; i < imageCount; i++)
  {
    std::vector<VkDescriptorBufferInfo> buffersInfo;
    buffersInfo.resize(bufferCount);

    for (uint32_t j = 0; j < bufferCount; j++)
    {
      buffersInfo[j] = {};
      buffersInfo[j].buffer = ubos[j]->buffers[i];
      buffersInfo[j].offset = 0;
      buffersInfo[j].range = ubos[j]->bufferSize;
    }

    VkWriteDescriptorSet descriptorWrite = {};
    descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
    descriptorWrite.dstSet = this->descriptorSets[i];
    descriptorWrite.dstBinding = 0;
    descriptorWrite.dstArrayElement = 0;
    descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
    descriptorWrite.descriptorCount = (uint32_t)buffersInfo.size();
    descriptorWrite.pBufferInfo = buffersInfo.data();

    vkUpdateDescriptorSets(gc->device, 1, &descriptorWrite, 0, nullptr);
  }
}

//
// FUNCIÓN:GEDescriptorSet::destroy(GEGraphicsContext* gc)
//
// PROPÓSITO: Destruye los conjuntos de descriptores
//
void GEDescriptorSet::destroy(GEGraphicsContext* gc) 
{
  vkFreeDescriptorSets(gc->device, descriptorPool, (uint32_t)descriptorSets.size(),
                                                            descriptorSets.data());
  vkDestroyDescriptorPool(gc->device, descriptorPool, nullptr);
}

 

 

Modificaciones de la clase CAFigure

 

Para incluir la información del bloque de variables uniformes se ha ampliado la clase GEFigure con los campos ubo (que contiene el objeto GEUniformBuffer utilizado para almacenar las variables uniformes) y dset (que contiene el objeto GEDescrptorSet con los conjuntos de descriptores). También se ha añadido el método update() para actualizar los valores de las variables almacenadas en los buffers.

#pragma once

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

//
// CLASE: GEFigure
//
// DESCRIPCIÓN: Clase que describe una figura formada por una malla de vértices
//
class GEFigure
{
public:
  GEFigure(GEGraphicsContext* gc, GERenderingContext* rc);
  void destroy(GEGraphicsContext* gc);
  void addCommands(VkCommandBuffer commandBuffer, VkPipelineLayout pipelineLayout, 
                                                                       int index);
  void update(GEGraphicsContext* gc, uint32_t index, GETransform transform);

private:
  GEVertexBuffer* vbo;
  GEIndexBuffer* ibo;
  GEUniformBuffer* ubo;
  GEDescriptorSet* dset;
};

El código de la clase modifica el constructor para crear los buffers vinculados a los bloques uniformes y los conjuntos de descriptores.  El método addCommands() añade el comando vkCmdBindDescriptorSets al proceso de dibujo de la figura. El método update() almacena nuevos valores de las variables uniformes en el buffer correspondiente a la imagen que se desea generar.

#include "GEFigure.h"

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

//
// FUNCIÓN: GEFigure::GEFigure(GEGraphicsContext* gc, GERenderingContext* rc)
//
// PROPÓSITO: Crea el Vertex Buffer
//
GEFigure::GEFigure(GEGraphicsContext* gc, GERenderingContext* rc)
{
  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());

  size_t transformBufferSize = sizeof(GETransform);
  ubo = new GEUniformBuffer(gc, rc->imageCount, transformBufferSize);

  std::vector<GEUniformBuffer*> ubos(1);
  ubos[0] = ubo;

  dset = new GEDescriptorSet(gc, rc, ubos);
}

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

  delete vbo;
  delete ibo;
  delete ubo;
  delete dset;
}

//
// FUNCIÓN: CAFigure::addCommands()
//
// PROPÓSITO: Añade los comandos de renderizado al command buffer
//
void GEFigure::addCommands(VkCommandBuffer commandBuffer, 
                           VkPipelineLayout pipelineLayout, 
                           int index)
{
  VkDeviceSize offset = 0;
  vkCmdBindVertexBuffers(commandBuffer, 0, 1, &(vbo->buffer), &offset);
  vkCmdBindIndexBuffer(commandBuffer, ibo->buffer, 0, VK_INDEX_TYPE_UINT16);
  vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, 
                          pipelineLayout, 0, 1, 
                          &(dset->descriptorSets[index]), 0, nullptr);
  vkCmdDrawIndexed(commandBuffer, (uint32_t)indices.size(), 1, 0, 0, 0);
}

//
// FUNCIÓN: GEFigure::update()
//
// PROPÓSITO: Actualiza las variables uniformes sobre una imagen del swapchain
//
void GEFigure::update(GEGraphicsContext* gc, uint32_t index, GETransform transform)
{
  ubo->update(gc, index, sizeof(GETransform), &transform);
}

 

 

Modificaciones de la clase GEScene

 

La clase GEScene desarrolla el modelo 3D que queremos mostrar en la pantalla. Los campos de la clase almacenan la referencia a la figura que vamos a mostrar (figure), la posición y tamaño del centro de este triángulo (posX, posY y size) y la velocidad de movimiento en cada eje (stepX y stepY).

#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;
  float posX;
  float posY;
  float size;
  float stepX;
  float stepY;

public:
  GEScene(GEGraphicsContext* gc, GEDrawingContext* dc, GECommandContext* cc);
  void destroy(GEGraphicsContext* gc);
  void recreate(GEGraphicsContext* gc, GEDrawingContext* dc, GECommandContext* cc);
  void update(GEGraphicsContext* gc, uint32_t index);
  void key_pressed(int key);

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

Se han añadido dos nuevos métodos de la clase (update() y key_pressed()) y se han tenido que realizar algunas modificaciones sobre el resto.

  • GEScene(): El constructor de la escena incluye ahora la inicialización de los campos de posicionamiento.

  • destroy(): No sufre modificaciones. Destruye la figura y el contexto de renderizado.

  • recreate(): Tampoco sufre modificaciones. Reconstruye el contexto de renderizado y los comandos.

  • addCommands(): Añade el campo pipelineLayout a la llamada a addCommands() de la figura.

  • update(): Genera la estructura GETransform y actualiza los buffers uniformes de la figura.

  • key_pressed(): Actualiza los variables de posicionamiento como respuesta a los eventos de teclado.

  • createPipelineConfig(): Modifica la configuración del pipeline para incluir un descriptor formado por una variable uniforme utilizada en el vertex shader.

El código de los métodos queda así:

#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, rc);
  this->rc->fillCommandBuffers(cc->commandBuffers, this);

  posX = 0.0f;
  posY = 0.0f;
  size = 0.20f;
  stepX = 0.02f;
  stepY = 0.01f;
}

//
// 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);
  this->rc->fillCommandBuffers(cc->commandBuffers, this);
}

//
// 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, rc->pipelineLayout, index);
}

//
// FUNCIÓN: GEScene::update(GEGraphicsContext* gc, uint32_t index)
//
// PROPÓSITO: Actualiza la información para generar la imagen 
//
void GEScene::update(GEGraphicsContext* gc, uint32_t index)
{
  GLfloat length = 0.5f * size;
  posX += stepX;
  if (posX - length < -1.0) { posX -= stepX; stepX = 0.02f; }
  if (posX + length > 1.0) { posX -= stepX; stepX = -0.02f; }

  posY += stepY;
  if (posY - length < -1.0) { posY -= stepY; stepY = 0.01f; }
  if (posY + length > 1.0) { posY -= stepY; stepY = -0.01f; }

  GETransform ubo;
  ubo.posX = posX;
  ubo.posY = posY;
  ubo.size = size;

  figure->update(gc, index, ubo);
}

//
// FUNCIÓN: GEScene::key_pressed(int)
//
// PROPÓSITO: Respuesta a acciones de teclado
//
void GEScene::key_pressed(int key)
{
  switch (key)
  {
  case GLFW_KEY_R:
    posX = 0.0f;
    posY = 0.0f;
    size = 0.2f;
    break;
  case GLFW_KEY_KP_ADD:
  case GLFW_KEY_1:
    size += 0.05f;
    if (size >= 1.0f) size = 1.0f;
    break;
  case GLFW_KEY_KP_SUBTRACT:
  case GLFW_KEY_2:
    size -= 0.05f;
    if (size <= 0.10f) size = 0.10f;
    break;
  }
}

//
// 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(1);
  config->descriptorTypes[0] = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
  config->descriptorStages.resize(1);
  config->descriptorStages[0] = VK_SHADER_STAGE_VERTEX_BIT;

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

  return config;
}

 

 

Modificaciones de la clase GEApplication

 

Con respecto a la clase GEApplication es necesario hacer algunas pequeñas modificaciones. Por una parte, la actualización de la escena se realiza en el método draw(), de manera que entrfe dos imágenes consecutivas se actualicen los valores de las variables de posicionamiento. El segundo cambio es en el método  keyCallback(), que debe llamar al método key_pressed() del objeto scene como respuesta a un evento de teclado.

//
// FUNCIÓN: GEApplication::draw()
//
// PROPÓSITO: Lanza la generación del dibujo
//
void GEApplication::draw()
{
  dc->waitForNextImage(gc);
  scene->update(gc, dc->getCurrentImage());
  dc->submitGraphicsCommands(gc, cc->commandBuffers);
  dc->submitPresentCommands(gc);
}

//
// FUNCIÓN: GEApplication::keyCallback()
//
// PROPÓSITO: Respuesta a un evento de teclado sobre la aplicación
//
void GEApplication::keyCallback(GLFWwindow* window, int key, int scancode, 
                                                             int action, int mods)
{
  GEApplication* app = (GEApplication*)glfwGetWindowUserPointer(window);
  if (action == GLFW_PRESS || action == GLFW_REPEAT)
  {
    if (key == GLFW_KEY_F12) app->swapFullScreen();
   else app->scene->key_pressed(key);
  }
}

 

 

Aspecto final

 

A continuación se muestran cuatro capturas de la aplicación en instantes diferentes. La aplicación muestra el triángulo moviéndose por la ventana y modificando su tamaño al pusar las teclas correspondientes.

Captura2 Captura3
Captura4 Captura5