Можно-ли изменить данные в массиве VAO?

272
26 ноября 2016, 18:59

Здравствуйте. Обращаюсь к специалистам OpenGL. Рисую трехмерную сцену с группой объектов VAO

for(auto VAO: vao_list)
{
    glBindVertexArray(VAO);
    glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_BYTE, nullptr);
    glBindVertexArray(0);
}

Все по "букварю", все работает. Но вот один из объектов, размером этак 40К вершин, потребовалось немного изменить: удалить 3 из 40 000 вершин. Можно конечно перезалить модифицированный объект, но для этого его образ надо вначале построить в оперативной памяти а потом перезалить в память графического процессора. А если меняющихся объектов в сцене большинство, то все их (копии) придется держать в оперативке, там править и перезаливать в графичекий буфер. Получается двойная работа, наверно тогда проще использовать "glDrawArrays", который рисует массивы прямо из оперативной памяти:

glEnableVertexAttribArray(idx_vertex_coord3d);
glEnableVertexAttribArray(idx_texture_coord);
glDrawArrays(GL_TRIANGLES, 0, count_of_vertices);
glDisableVertexAttribArray(idx_texture_coord);
glDisableVertexAttribArray(idx_vertex_coord3d);

но в этом случае память графического процессора (очень дорогая и быстрая) простаивает. Вот собственно вопрос: а можно ли как-то частично изменять объект размещенный в VAO (в графической памяти), удаляя/добавляя часть вершин в нем не перезаписывая VAO полностью?

Answer 1
VAO и VBO

На всякий случай вначале скажу немного про VAO и VBO. VAO не хранит данные объекта, а хранит только ссылки на один или несколько VBO и прочее состояние, необходимое для рисования объекта. Поэтому работать мы будем с VBO. Если несколько атрибутов вершин хранятся в нескольких VBO, то все их придётся править по отдельности. Изменение VBO будет сразу же доступно пользователям связанного VAO.

Рассуждения об удалении

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

glDrawElements(GL_TRIANGLE_STRIP, num_elements - 3, GL_UNSIGNED_BYTE, nullptr);

вместо

glDrawElements(GL_TRIANGLE_STRIP, num_elements, GL_UNSIGNED_BYTE, nullptr);

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

int a[] = {1,2,3,4,5,6};

то мы не можем удалить из него элементы 2 и 3, не оставив на их месте нули или какие-нибудь специальные значения значения, указывающие на то, что данные удалены. Чтобы не оставлять этих значений, мы можем либо перевыделить память под новый массив нужной длины и скопировать туда значения, либо скопировать 5 и 6 на место 2 и 3 и запомнить, что теперь длина массива равна 4 вместо 6. Оставшаяся в хвосте память под 2 элемента будет потрачена впустую, но при 40000 элементов ей можно пренебречь.

То же самое можно применить и к массиву в видеопамяти:

  • Можно вызвать glBufferData для выделения нового буфера.
  • Можно заменить кусок данных от начала удаляемого куска до конца буфера новыми значениями с помощью glBufferSubData, не забыв про уменьшение num_elements.
  • Сдвинуть данные копированием внутри буфера не получится, ибо регионы памяти при копировании внутри буфера не должны пересекаться.

Вышеперечисленные методы могут иметь смысл при изменении/"удалении" большого количества данных, но, как вы заметили, совершенно не подходят для удаления трёх точек из 40000. То есть полностью удалить точки не получится, и придётся искать обходной путь.

Возможное решение

Функция glBufferSubData позволяет указывать смещение и длину заменяемых данных. Поэтому с её помощью можно переписать как весь буфер, так и любую его часть, хоть один единственный элемент. Если нам нужно удалить 3 из 40000 точек из модели, то не обязательно их удалять из буфера! Давайте просто перезапишем их значениями подходящей соседней вершины. Например, пусть есть ломаная из 8 вершин на плоскости:

   2       3     5    7
   /-------\    /\    /----8
  /         \  /  \  /
 /           \/    \/
/1           4      6 

Если мы сделаем вершину 5 равной вершине 6, то получим тот же эффект, что и при удалении вершины 5: появится отрезок, визуально соединяющий вершины 4 и 6, а длина отрезка 5-6 станет нулевой:

   2       3          7
   /-------\          /----8
  /         \        /
 /           \------/
/1           4      5,6 

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

Usage pattern

Из вашего описания кажется, что "удалять" удалять точки вы будете редко, т.е. не на каждом кадре. Буфер же будет использоваться только для вывода графики, но не для transform feedback и прочих таких хитростей. Если так, то при создании буфера не забудьте в последнем аргументе glBufferData указать GL_DYNAMIC_DRAW. Однако, без тестов производительности конкретного приложения здесь нельзя давать однозначную рекомендацию. При очень редком обновлении буфера GL_STATIC_DRAW может оказаться быстрее. Напротив, при очень частом лучшим выбором может быть GL_STREAM_DRAW.

Небольшой пример

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

GLuint vboId;
GLfloat* vertices = new GLfloat[vCount*3]; // элемент из 3 вершин
...
// запросим индекс для нового VBO
glGenBuffersARB(1, &vboId);
glBindBufferARB(GL_ARRAY_BUFFER_ARB, vboId);
// передаем данные в VBO
glBufferDataARB(GL_ARRAY_BUFFER_ARB, dataSize, vertices, GL_STATIC_DRAW_ARB);
// освободим память после переноса данных в VBO
delete [] vertices;
...
// В нужный момент этот VBO можно удалить
glDeleteBuffersARB(1, &vboId);
READ ALSO
undefined reference to operator<< [дубликат]

undefined reference to operator<< [дубликат]

На данный вопрос уже ответили:

261
Несколько мод числового ряда c++

Несколько мод числового ряда c++

Делаю задания по книге Бьярне Страуструпа "Программирование: принципы и практика с использованием C++, 2-е издание"Во главе 4, задании 16 надо...

283
MinGW или Cygwin?

MinGW или Cygwin?

Раньше просто компилировал с помощью g++ на Ubuntu, вытягивая компилятор с помощью пакетного менеджера:

358
стандарты GNU C++ и GNU C++11

стандарты GNU C++ и GNU C++11

Почему gcc компилирует этот код для стандарта GNU C++, но не компилирует для GNU C++11?

261