Vulkan API — урок 38. Буфер индексов (+листинг)

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

Т.е. описанный ранее метод потребует ввода трех вершин для каждого треугольника, но думаю все согласны, что такой подход по меньшей мере глуп. А если это уже нечто полноценное, а если оно еще и в 3D. Решение подобных задач таится в буфере индексов (index buffer).

Буфер индексов, по существу является, массивом указателей на элементы буфера вершин. Он позволяет изменить порядок вершин и повторно использовать имеющиеся данные для нескольких вершин, но это, думаю, итак уже ясно из картинки.

Создание буфера индексов

Добавим информацию о четвертом угле:

const std::vector<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}}
};

Верхний левый угол – красный, верхний правый – зеленый, нижний правый – синий, нижний левый – белый. Добавим новый массив indices для отражения содержимого буфера индексов. Он должен соответствовать индексам на изображении для отрисовки верхнего правого и нижнего левого треугольника:

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

В данном случае используется uint16_t, т.к. количество вершин не превысит 65535. Аналогично данным вершин, индексы должны быть загружены VkBuffer в для GPU.

Определение двух новых членов класса для хранения ресурсов буфера индексов:

VDeleter<VkBuffer> vertexBuffer{device, vkDestroyBuffer};
VDeleter<VkDeviceMemory> vertexBufferMemory{device, vkFreeMemory};
VDeleter<VkBuffer> indexBuffer{device, vkDestroyBuffer};
VDeleter<VkDeviceMemory> indexBufferMemory{device, vkFreeMemory};

Функция createIndexBuffer, что сейчас добавим, практически полностью совпадает с createVertexBuffer:

void initVulkan() {
    ...
    createVertexBuffer();
    createIndexBuffer();
    ...
}

void createIndexBuffer() {
    VkDeviceSize bufferSize = sizeof(indices[0]) * indices.size();

    VDeleter<VkBuffer> stagingBuffer{device, vkDestroyBuffer};
    VDeleter<VkDeviceMemory> stagingBufferMemory{device, vkFreeMemory};
    createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory);

    void* data;
    vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data);
    memcpy(data, indices.data(), (size_t) bufferSize);
    vkUnmapMemory(device, stagingBufferMemory);

    createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, indexBuffer, indexBufferMemory);

    copyBuffer(stagingBuffer, indexBuffer, bufferSize);
}

Имеются только два заметных отличия. Теперь bufferSize зависит от количества индексов и их размера (uint16_t или uint32_t, насчет поддержки 64 в данный конкретный момент достоверной информации не скажу, но даже на сайте разрабов указана возможность выбора только между данными типами). Использование indexBuffer должно иметь VK_BUFFER_USAGE_INDEX_BUFFER_BIT вместо VK_BUFFER_USAGE_VERTEX_BUFFER_BIT. Кроме этого функция аналогична, создаем промежуточный буфер, а затем его содержимое переносим в локальный буфер устройства.

Создание буфера индексов

Использование буфера индексов для отрисовки включает в себя два изменения в createCommandBuffers. Для начала понадобится привязать буфер индексов, так же как это делалось с буфером вершин. Разница заключается в том, что можно иметь только один буфер индексов. К сожалению использовать разные индексы для каждого атрибута вершин не представляется возможным, потому придется полностью копировать данные вершины, если даже единственный параметр отличается.

vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets);

vkCmdBindIndexBuffer(commandBuffers[i], indexBuffer, 0, VK_INDEX_TYPE_UINT16);

Буфер индексов связывается при помощи vkCmdBindIndexBuffer, который принимает буфер индексов, смещение в нем и тип индексов. Как упоминалось ранее, возможны только VK_INDEX_TYPE_UINT16 и VK_INDEX_TYPE_UINT32.

Далее скажем Vulkan’у, что нужно использовать буфер индексов. Заменим строку vkCmdDraw на vkCmdDrawIndexed:

vkCmdDrawIndexed(commandBuffers[i], indices.size(), 1, 0, 0, 0);

Сама функция очень похожа на предыдущую. Первые два параметра и так понятны. Мы не используем instancing рендеринг, потому все так же остается значение 1. Следующий параметр – смещение, использование значения 1 приведет к тому, что видеокарта начнет считать со второго индекса. Предпоследний параметр определяет смещение, которое нужно будет добавить к индексам в буфере индексов. Последний параметр определяет смещение для instancing, что мы не используем.

И теперь на экране должно появиться это чудо:

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

Осталось 4 базовые темы связанные с Vulkan’ом,которые нужно рассказать. В следующем уроке мы начнем говорим о том, что такое 3D.

А пока листинг.

Main Admin

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

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