[Перевод] Vulkan. Руководство разработчика. Индексный буфер
Я продолжаю выкладывать переводы тьюториала к Vulkan на русский язык (оригинальный текст тьюториала можно найти здесь). В сегодняшней публикации представлен перевод заключительной статьи раздела Vertex buffers, которая называется Index buffer.
Индексный буфер
Вступление
Во многих мешах, которые вы будете рендерить в реальных приложениях, одна вершина может быть общей для большого числа треугольников. Такое встречается даже в простом прямоугольнике:
Для отрисовки прямоугольника нужны два треугольника, поэтому нам понадобится вершинный буфер с 6 вершинами. Но проблема в том, что данные двух вершин необходимо дублировать, что приводит к 50% -ной избыточности. С более сложными объектами, где одна вершина повторно используется в среднем в 3 треугольниках, ситуация становится еще хуже. Для решения этой проблемы используется индексный буфер.
Индексный буфер — это, по сути, массив указателей на данные внутри вершинного буфера. Он позволяет перегруппировать данные вершин и повторно использовать существующие данные для нескольких вершин. На рисунке выше показано, как будет выглядеть индексный буфер для прямоугольника, если вершинный буфер не содержит повторений и состоит только из четырех вершин. Первые три индекса определяют правый верхний треугольник, а последние три индекса — нижний левый треугольник.
Создание индексного буфера
В этой главе мы изменим данные вершин и добавим данные индексов, чтобы нарисовать прямоугольник, подобный тому, что изображен на картинке выше. Изменим данные вершин, чтобы представить четыре угла:
const std::vector 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 indices = {
0, 1, 2, 2, 3, 0
};
Для индексного буфера можно использовать uint16_t
или uint32_t
в зависимости от количества элементов в vertices
. Пока мы будем использовать uint16_t
, поскольку у нас менее 65535 уникальных вершин.
Как и данные вершин, индексы должны быть загружены в VkBuffer
, чтобы GPU мог получить к ним доступ. Определим два новых члена класса для хранения ресурсов индексного буфера:
VkBuffer vertexBuffer;
VkDeviceMemory vertexBufferMemory;
VkBuffer indexBuffer;
VkDeviceMemory indexBufferMemory;
Добавим функцию createIndexBuffer
, которая практически идентична createVertexBuffer
:
void initVulkan() {
...
createVertexBuffer();
createIndexBuffer();
...
}
void createIndexBuffer() {
VkDeviceSize bufferSize = sizeof(indices[0]) * indices.size();
VkBuffer stagingBuffer;
VkDeviceMemory stagingBufferMemory;
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);
vkDestroyBuffer(device, stagingBuffer, nullptr);
vkFreeMemory(device, stagingBufferMemory, nullptr);
}
Есть лишь два заметных отличия. Теперь bufferSize
равен количеству индексов, умноженному на размер типа индекса — uint16_t
или uint32_t
. И вместо VK_BUFFER_USAGE_VERTEX_BUFFER_BIT
используется VK_BUFFER_USAGE_INDEX_BUFFER_BIT
, что вполне логично. В остальном все то же самое. Мы создаем промежуточный буфер, чтобы скопировать в него содержимое indices
, и уже из него скопировать данные в конечный локальный индексный буфер устройства.
Как и вершинный буфер, в конце работы программы индексный буфер нужно удалить:
void cleanup() {
cleanupSwapChain();
vkDestroyBuffer(device, indexBuffer, nullptr);
vkFreeMemory(device, indexBufferMemory, nullptr);
vkDestroyBuffer(device, vertexBuffer, nullptr);
vkFreeMemory(device, vertexBufferMemory, nullptr);
...
}
Использование индексного буфера
Чтобы использовать индексный буфер, нужно внести пару изменений в createCommandBuffers
. Сначала нужно привязать (bind) индексный буфер, как мы это делали с вершинным буфером. Разница в том, что индексный буфер может быть только один. К сожалению, нет способа использовать разные индексные буферы для каждого из атрибутов вершин. Поэтому, если 2 вершины отличаются, скажем, только по цвету, то нам всё равно придется дублировать их координаты.
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], static_cast(indices.size()), 1, 0, 0, 0);
Вызов этой функции очень похож на vkCmdDraw
. Первые два параметра определяют количество индексов и количество экземпляров (instances). Мы не используем инстансинг, поэтому укажем только 1
экземпляр. Количество индексов представляет собой количество вершин, которые будут переданы в вершинный буфер. Следующий параметр определяет смещение в индексном буфере. Если бы мы использовали значение 1
, видеокарта начала бы считывать данные со второго индекса. Предпоследний параметр указывает смещение, добавляемое к индексам в индексном буфере. Последний параметр указывает смещение для инстансинга, который мы не используем.
Если запустить программу, в результате должно получиться следующее:
Теперь вы знаете, как сэкономить память, повторно используя вершины с помощью индексного буфера. Это особенно пригодится, когда мы будем загружать сложные 3D-модели.
В предыдущих главах уже упоминалось о том, что вы должны выделять память сразу под несколько ресурсов, таких как буферы, но вы можете пойти еще дальше. Разработчики драйверов рекомендуют хранить несколько буферов, например вершинный и индексный буфер, в одном VkBuffer
и использовать смещения в таких командах, как vkCmdBindVertexBuffers
. Преимущество этого в том, что тогда данные намного удобнее кешировать, поскольку они находятся ближе друг к другу. Более того, можно повторно использовать один и тот же кусок памяти для нескольких ресурсов, если они не используются во время одних и тех же операций рендеринга, при условии, конечно, что данные будут обновляться. Это называется алиасингом (наложение), и в некоторых функциях Vulkan есть явные флаги, указывающие на то, что вы хотите его использовать.
Код C++
/ Вершинный шейдер
/ Фрагментный шейдер