Я продолжаю выкладывать переводы тьюториала к Vulkan на русский язык (оригинальный текст тьюториала можно найти здесь). В сегодняшней публикации представлен перевод заключительной статьи раздела Vertex buffers, которая называется Index buffer.

Содержание
1. Вступление

2. Краткий обзор

3. Настройка окружения

4. Рисуем треугольник

  1. Подготовка к работе
  2. Отображение на экране
  3. Графический конвейер (pipeline)
  4. Отрисовка
  5. Пересоздание swap chain

5. Вершинные буферы

  1. Описание входных данных вершин
  2. Создание вершинного буфера
  3. Промежуточный буфер
  4. Индексный буфер

6. Uniform-буферы

  1. Layout дескрипторов и буфер
  2. Пул дескрипторов и сеты дескрипторов

7. Текстурирование

  1. Изображения
  2. Image view и image sampler
  3. Комбинированный image sampler

8. Буфер глубины

9. Загрузка моделей

10. Создание мип-карт

11. Multisampling

FAQ

Политика конфиденциальности


Индексный буфер




Вступление


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



Для отрисовки прямоугольника нужны два треугольника, поэтому нам понадобится вершинный буфер с 6 вершинами. Но проблема в том, что данные двух вершин необходимо дублировать, что приводит к 50% -ной избыточности. С более сложными объектами, где одна вершина повторно используется в среднем в 3 треугольниках, ситуация становится еще хуже. Для решения этой проблемы используется индексный буфер.

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

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


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

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 или 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<uint32_t>(indices.size()), 1, 0, 0, 0);

Вызов этой функции очень похож на vkCmdDraw. Первые два параметра определяют количество индексов и количество экземпляров (instances). Мы не используем инстансинг, поэтому укажем только 1 экземпляр. Количество индексов представляет собой количество вершин, которые будут переданы в вершинный буфер. Следующий параметр определяет смещение в индексном буфере. Если бы мы использовали значение 1, видеокарта начала бы считывать данные со второго индекса. Предпоследний параметр указывает смещение, добавляемое к индексам в индексном буфере. Последний параметр указывает смещение для инстансинга, который мы не используем.

Если запустить программу, в результате должно получиться следующее:



Теперь вы знаете, как сэкономить память, повторно используя вершины с помощью индексного буфера. Это особенно пригодится, когда мы будем загружать сложные 3D-модели.

В предыдущих главах уже упоминалось о том, что вы должны выделять память сразу под несколько ресурсов, таких как буферы, но вы можете пойти еще дальше. Разработчики драйверов рекомендуют хранить несколько буферов, например вершинный и индексный буфер, в одном VkBuffer и использовать смещения в таких командах, как vkCmdBindVertexBuffers. Преимущество этого в том, что тогда данные намного удобнее кешировать, поскольку они находятся ближе друг к другу. Более того, можно повторно использовать один и тот же кусок памяти для нескольких ресурсов, если они не используются во время одних и тех же операций рендеринга, при условии, конечно, что данные будут обновляться. Это называется алиасингом (наложение), и в некоторых функциях Vulkan есть явные флаги, указывающие на то, что вы хотите его использовать.

Код C++ / Вершинный шейдер / Фрагментный шейдер

Комментарии (0)