Введение
В прошлой заметке был получен результат рендера в виде цветного прямоугольника, но программа на данном этапе является статической и не может загружать модели произвольной геометрии.
Важное замечание: на текущий момент вершины не будут храниться в оперативной памяти после загрузки в память графического устройства.
Содержание заметки:
- переработка буферов данных и рендера кадра;
- изменение способа доступа к объекту класса Vulkan;
- абстрактный класс модели;
- класс модели без индексного буфера;
- класс модели с индексным буфером;
- список моделей для рендера.
Переработка буферов данных и рендера кадра
На данный момент программа не способна создавать буферы данных для работы с произвольной геометрией и лишь способна выводить прямоугольник.
Необходимо добавить приватное поле-словарь к классу Vulkan. Ключом будет являться дескриптор буфера, а в качестве значений используется пара (std::pair) дескрипторов логического устройства и памяти с которыми связан буфер данных. Пример словаря:
std::map<VkBuffer, std::pair<VkDevice, VkDeviceMemory>> databuffers;
Теперь необходимо добавить буфер в словарь при создании, для этого дополним метод Vulkan::createBuffer:
// Сохраним дескрипторы логического устройства и памяти буфера
// по ключу дескриптора буфера
databuffers[buffer] = {logicalDevice, bufferMemory};
Теперь дескриптор памяти доступен по словарю и ссылку на него можно убрать из параметров:
void Vulkan::createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer& buffer)
Создадим метод для удаления буфера данных:
void Vulkan::destroyBuffer(VkBuffer buffer)
{
vkDeviceWaitIdle(logicalDevice); // Ожидание окончания асинхронных задач
vkDestroyBuffer(databuffers[buffer].first /*logicalDevice*/, buffer, nullptr); // Уничтожение буфера
vkFreeMemory(databuffers[buffer].first /*logicalDevice*/, databuffers[buffer].second /*VkDeviceMemory*/, nullptr); // Освобождение памяти буфера
databuffers.erase(buffer); // Удалим ключ из словаря
}
Важное замечание: необходимо выполнить ожидание завершения асинхронных задач перед удалением буферов (отсутствует на теге v0.1 и добавлено в конце заметки).
Заменим ручное удаление буферов данных в методе Vulkan::destroy циклом:
// Освобождение буферов данных
for (const auto& kv : databuffers)
{
vkDestroyBuffer(kv.second.first /*logicalDevice*/, kv.first /*VkBuffer*/, nullptr); // Уничтожение буфера
vkFreeMemory(kv.second.first /*logicalDevice*/, kv.second.second /*VkDeviceMemory*/, nullptr); // Освобождение памяти буфера
}
databuffers.clear();
Объединим методы createVertexBuffer и createIndexBuffer в createDataBuffer с учетом нового метода destroyBuffer:
// Создание и инициализация буфера данных
VkBuffer Vulkan::createDataBuffer(void* data, VkDeviceSize size, VkBufferUsageFlags usage)
{
VkBuffer result;
// Промежуточный буфер для переноса на устройство
VkBuffer stagingBuffer;
createBuffer(size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer);
// Отображение памяти буфера
void* stagingData;
vkMapMemory(logicalDevice, databuffers[stagingBuffer].second /*VkDeviceMemory*/, 0, size, 0, &stagingData);
// Копирование данных в промежуточный буфер
memcpy(stagingData, data, (size_t) size);
// Прекращение отображения памяти буфера
vkUnmapMemory(logicalDevice, databuffers[stagingBuffer].second /*VkDeviceMemory*/);
// Создание буфера данных
createBuffer(size, VK_BUFFER_USAGE_TRANSFER_DST_BIT | usage, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, result);
// Копирование из промежуточного в буфер данных
copyBuffer(stagingBuffer, result, size);
// Освобождение памяти и уничтожение буферов
destroyBuffer(stagingBuffer);
return result;
}
Теперь можно заменить вызовы внутри метода init следующим кодом:
//Вершины, записываемые в буфер
Vertex vertices[] ={
{ {-0.5f, -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} },
{ {-0.5f, 0.5f}, {1.0f, 1.0f, 1.0f} }
};
// Размер буфера в байтах
VkDeviceSize bufferSize = sizeof(Vertex) * 4;
vertexBuffer = createDataBuffer(vertices, bufferSize, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); // Создание буфера вершин
// Индексы, записываемые в буфер
uint16_t indices[] = {0, 1, 2, 2, 3, 0};
// Размер буфера в байтах
bufferSize = sizeof(uint16_t) * 6;
indexBuffer = createDataBuffer(indices, bufferSize, VK_BUFFER_USAGE_INDEX_BUFFER_BIT); // Создание буфера индексов
Для обеспечения читаемости кода выделим из метода renderFrame: renderBegin и renderEnd. Так как переменная imageIndex теперь используется в нескольких методах — сделаем её приватным полем класса Vulkan.
Пример разделения:
// Рендер кадра
void Vulkan::renderFrame()
{
renderBegin();
VkBuffer vertexBuffers[] = {vertexBuffer};
VkDeviceSize offsets[] = {0};
vkCmdBindVertexBuffers(commandBuffers[currentFrame], 0, 1, vertexBuffers, offsets);
vkCmdBindIndexBuffer(commandBuffers[currentFrame], indexBuffer, 0, VK_INDEX_TYPE_UINT16);
vkCmdDrawIndexed(commandBuffers[currentFrame], 6, 1, 0, 0, 0);
renderEnd();
}
// Начало рендера кадра
void Vulkan::renderBegin()
{
vkWaitForFences(logicalDevice, 1, &inWorkFences[currentFrame], VK_TRUE, UINT64_MAX);
vkResetFences(logicalDevice, 1, &inWorkFences[currentFrame]);
VkResult result = vkAcquireNextImageKHR(logicalDevice, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);
if (result != VK_SUCCESS)
{
throw std::runtime_error("Unable to acquire swap chain image");
}
vkResetCommandBuffer(commandBuffers[currentFrame], 0);
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
if (vkBeginCommandBuffer(commandBuffers[currentFrame], &beginInfo) != VK_SUCCESS)
{
throw std::runtime_error("Unable to begin recording command buffer");
}
VkClearValue clearColor = {0.0f, 0.0f, 0.0f, 1.0f};
VkRenderPassBeginInfo renderPassInfo{};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
renderPassInfo.renderPass = renderPass;
renderPassInfo.framebuffer = swapChainFramebuffers[imageIndex];
renderPassInfo.renderArea.offset = {0, 0};
renderPassInfo.renderArea.extent = surface.selectedExtent;
renderPassInfo.clearValueCount = 1;
renderPassInfo.pClearValues = &clearColor;
vkCmdBeginRenderPass(commandBuffers[currentFrame], &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);
vkCmdBindPipeline(commandBuffers[currentFrame], VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);
}
// Окончание рендера кадра
void Vulkan::renderEnd()
{
vkCmdEndRenderPass(commandBuffers[currentFrame]);
if (vkEndCommandBuffer(commandBuffers[currentFrame]) != VK_SUCCESS)
{
throw std::runtime_error("Unable to record command buffer");
}
VkSemaphore waitSemaphores[] = {imageAvailableSemaphores[currentFrame]};
VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
VkSemaphore signalSemaphores[] = {renderFinishedSemaphores[currentFrame]};
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = waitSemaphores;
submitInfo.pWaitDstStageMask = waitStages;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffers[currentFrame];
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = signalSemaphores;
if (vkQueueSubmit(queue.descriptor, 1, &submitInfo, inWorkFences[currentFrame]) != VK_SUCCESS)
{
throw std::runtime_error("Unable to submit draw command buffer");
}
currentFrame = (currentFrame + 1) % surface.imageCount;
VkPresentInfoKHR presentInfo{};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = signalSemaphores;
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = &swapChain;
presentInfo.pImageIndices = &imageIndex;
if (vkQueuePresentKHR(queue.descriptor, &presentInfo) != VK_SUCCESS)
{
throw std::runtime_error("Unable to present swap chain image");
}
}
Для дальнейшей работы с классами модели необходимо перенести методы createDataBuffer и destroyBuffer в публичную область видимости.
Текущий вариант доступен на теге v0.1 репозитория 07.
Изменение способа доступа к объекту класса Vulkan
Переместим объект класса Vulkan из функции main в файл Vulkan.cpp, а в функции main.cpp подключим с ключевым словом extern. Данный подход позволит использовать один и тот же объект класса в разных частях программы.
main.cpp:
...
int main(int argc, char* argv[])
{
// объект класса-обертки Vulkan API
extern Vulkan vulkan;
...
Vulkan.cpp:
...
#include "macroses.h"
// объект класса-обертки Vulkan API
Vulkan vulkan;
// инициализация
void Vulkan::init(GLFWwindow* window)
...
Абстрактный класс модели
Для обеспечения необходимой гибкости приложения будет использован базовый абстрактный класс модели, реализующий интерфейс для работы с различными реализациями классов, содержащих в себе информацию о геометрии объектов.
Необходимо создать новый заголовочный файл: I_Model.h.
Важное примечание: все файлы, содержащие в себе интерфейсы начинаются с заглавной латинской буквы I
Пример абстрактного класса:
class I_Model
{
public:
virtual ~I_Model() {};
virtual void render(VkCommandBuffer commandBuffer) = 0;
protected:
virtual void bindBuffers(VkCommandBuffer commandBuffer) = 0;
};
На данный момент класс имеет следующие методы:
- render (публичный) — запись команд в командный буфер для рендера модели в текущий кадр;
- bindBuffers (защищенный) — запись команды привязки буферов данных.
Важное примечание: деструкторы обязательно должны быть виртуальными, иначе не получится добиться их корректной работы при полиморфизме.
Класс модели без индексного буфера
На основании существующего интерфейса необходимо создать класс, содержащий данные о геометрии модели без использования индексного буфера.
Необходимо создать новый заголовочный файл: Model.h, внутри которого нужно подключить файл с интерфейсом I_Model.h, а так же Model.cpp для реализаций методов.
Пример класса модели без индексов:
class Model_wo_indexes : public I_Model
{
public:
Model_wo_indexes(Vertex* vertexArray, uint32_t verteciesCount);
virtual ~Model_wo_indexes();
virtual void render(VkCommandBuffer commandBuffer);
protected:
void bindBuffers(VkCommandBuffer commandBuffer); // привязка используемых буферов данных
private:
uint32_t verteciesCount; // Количество вершин
VkBuffer vertexBuffer; // Буфер вершин
};
Примечание: в продолжении на основании данного класса будет создан класс, использующий буфер индексов, и для обеспечения доступа к дескриптору вершинного буфера необходим отдельный метод bindBuffers.
В приватной области класса хранятся количество вершин verteciesCount и дескриптор вершинного буфера vertexBuffer.
Конструктор класса принимает массив вершин и его размер, на основании которых создает и инициализирует буфер данных:
Model_wo_indexes::Model_wo_indexes(Vertex* vertexArray, uint32_t count) : verteciesCount(count)
{
extern Vulkan vulkan;
// Создание и инициализация буфера вершин
vertexBuffer = vulkan.createDataBuffer(vertexArray, verteciesCount * sizeof(Vertex), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
}
Деструктор уничтожает созданный буфер:
Model_wo_indexes::~Model_wo_indexes()
{
extern Vulkan vulkan;
vulkan.destroyBuffer(vertexBuffer);
}
Метод bindBuffers записывает команду привязки буфера вершин:
void Model_wo_indexes::bindBuffers(VkCommandBuffer commandBuffer)
{
VkBuffer vertexBuffers[] = {vertexBuffer};
VkDeviceSize offsets[] = {0};
vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets);
}
Метод render вызывает привязку буфера вершин и записывает команду рисования вершин:
void Model_wo_indexes::render(VkCommandBuffer commandBuffer)
{
bindBuffers(commandBuffer);
vkCmdDraw(commandBuffer, verteciesCount, 1, 0, 0);
}
Класс модели с индексным буфером
Возьмем созданный ранее класс модели без индексов как основу для класса модели с индексным буфером.
Пример класса модели с индексным буфером:
class Model_w_indexes : public Model_wo_indexes
{
public:
Model_w_indexes(Vertex* vertexArray, uint32_t verteciesCount, uint32_t* indexArray, uint32_t indeciesCount);
virtual ~Model_w_indexes();
virtual void render(VkCommandBuffer commandBuffer);
protected:
void bindBuffers(VkCommandBuffer commandBuffer); // привязка используемых буферов данных
private:
uint32_t indeciesCount; // Количество индексов
VkBuffer indexBuffer; // Буфер индексов
};
Конструктор класса принимает массив вершин и индексов и их размеры, на основании которых создает и инициализирует буфер данных, содержащий индексы вершин (вершинный буфер создается родительским конструктором):
Model_w_indexes::Model_w_indexes(Vertex* vertexArray, uint32_t vCount, uint32_t* indexArray, uint32_t iCount)
: Model_wo_indexes(vertexArray, vCount), indeciesCount(iCount)
{
extern Vulkan vulkan;
// Создание и инициализация буфера индексов
indexBuffer = vulkan.createDataBuffer(indexArray, indeciesCount * sizeof(uint32_t), VK_BUFFER_USAGE_INDEX_BUFFER_BIT);
}
Деструктор уничтожает только буфер индексов — о буфере вершин позаботится родительский деструктор:
Model_w_indexes::~Model_w_indexes()
{
extern Vulkan vulkan;
vulkan.destroyBuffer(indexBuffer);
}
Метод bindBuffers записывает команды привязки буферов вершин (с помощью родительского метода) и индексов:
void Model_w_indexes::bindBuffers(VkCommandBuffer commandBuffer)
{
// Привязка родительских буферов
Model_wo_indexes::bindBuffers(commandBuffer);
// Привязка индексного буфера
vkCmdBindIndexBuffer(commandBuffer, indexBuffer, 0, VK_INDEX_TYPE_UINT32);
}
Метод render вызывает привязку буферов и записывает команду рисования вершин по индексам:
void Model_w_indexes::render(VkCommandBuffer commandBuffer)
{
bindBuffers(commandBuffer);
vkCmdDrawIndexed(commandBuffer, indeciesCount, 1, 0, 0, 0);
}
Список моделей для рендера
Создадим список моделей, который необходимо рендерить в кадре. Позднее данный список будет преобразован в сцену. В качестве контейнера будет использоваться std::list указателей на интерфейс модели (I_Model):
std::list<I_Model*> renderList; // Спискок моделей для рендера
Для взаимодействия со списком необходимо добавить следующие методы:
- addToRenderList — добавление модели в список для рендера;
- removeFromRenderList — удаление модели из списка для рендера;
- clearRenderList — очистка списка рендера.
Пример их реализации:
// Добавление модели в список рендера
void Vulkan::addToRenderList(I_Model* model)
{
if (model)
renderList.push_back(model);
}
// Удаление модели из список рендера
void Vulkan::removeFromRenderList(I_Model* model)
{
if (model)
renderList.remove(model);
}
// Очистка списка рендера моделей
void Vulkan::clearRenderList()
{
renderList.clear();
}
Добавим вызовы команд render от объектов, находящихся в списке рендера, к методу Vulkan::renderFrame:
// Рендер кадра
void Vulkan::renderFrame()
{
renderBegin(); // Начало рендера
for (auto const& model : renderList)
{
model->render(commandBuffers[currentFrame]);
}
renderEnd(); // Конец рендера
}
Теперь можно создать модель в функции main и добавить её к очереди рендера:
model = new Model_w_indexes(vertices, 4, indices, 6);
vulkan.addToRenderList(model);
Важное замечание: важно создавать и удалять модели после создания и до уничтожения экземпляра Vulkan (методы init и destroy).
Необходимо не забыть удалить модель, под которую была выделена память:
delete model;
Если какой-то буфер не будет удален до вызова метода destroy — внутри него будет произведено удаление оставшихся буферов.
Изменим метод Vulkan::destroyBuffer добавив проверку наличия буфера в словаре Vulkan::databuffers:
void Vulkan::destroyBuffer(VkBuffer buffer)
{
// Проверка что такой буфер ещё существует
if (databuffers.find(buffer) != databuffers.end())
{
vkDeviceWaitIdle(logicalDevice); // Ожидание окончания асинхронных задач
vkDestroyBuffer(databuffers[buffer].first /*logicalDevice*/, buffer, nullptr); // Уничтожение буфера
vkFreeMemory(databuffers[buffer].first /*logicalDevice*/, databuffers[buffer].second /*VkDeviceMemory*/, nullptr); // Освобождение памяти буфера
databuffers.erase(buffer); // Удалим ключ из словаря
}
}
Текущий вариант доступен на теге v0.2 репозитория 07.
Вывод
В рамках данной заметки был переработан принцип работы с буферами данных, созданы классы моделей и список для рендера. В дальнейших заметках будет создан класс-фабрика для загрузки моделей из файлов.
Проект доступен в публичном репозитории: 07
Библиотеки: dependencies