|
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.
|
|