Рубрики
3D мир Программирование

Vulkan API 7: класс модели

Данная заметка посвящена работе над классом модели с произвольной геометрией

Введение

В прошлой заметке был получен результат рендера в виде цветного прямоугольника, но программа на данном этапе является статической и не может загружать модели произвольной геометрии.

Важное замечание: на текущий момент вершины не будут храниться в оперативной памяти после загрузки в память графического устройства.

Содержание заметки:

Переработка буферов данных и рендера кадра

На данный момент программа не способна создавать буферы данных для работы с произвольной геометрией и лишь способна выводить прямоугольник.

Необходимо добавить приватное поле-словарь к классу 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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.