Здравствуйте. Обращаюсь к специалистам 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 полностью?
Ответ
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
Комментарий от автора вопроса
Небольшой пример. Спасибо, все встало на свои места. В добавление к вашему ответу хочу добавить демо-код для одного из перечисленных вами вариантов решения:
GLfloat* vertices = new GLfloat[9]; // элемент из 3 вершин
...
GLuint pos_Buf;
glGenBuffers(1, &pos_Buf);
// передаем данные в GPU
glBindBuffer(GL_ARRAY_BUFFER, pos_Buf);
glBufferData(GL_ARRAY_BUFFER, 9*sizeof(float), vertices, GL_STATIC_DRAW);
// освободить память после переноса данных в GPU
delete [] vertices;
...
// Если надо изменить, например, седьмой элемент массива:
// Пример изменения данных, размещенных в графической памяти
// Вариант - glUnmapBuffer
// ----------------------------------------------------
glBindBuffer(GL_ARRAY_BUFFER, pos_Buf);
data = (GLfloat *) glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
data[7] = 0.4f;
glUnmapBuffer(GL_ARRAY_BUFFER);
...
// Вариант 2 - glBufferSubData
// ----------------------------------------------------
glBindBuffer(GL_ARRAY_BUFFER, pos_Buf);
GLintptr offset = 7*sizeof(float);
float pos_SubD[] = {0.0f};
glBufferSubData(GL_ARRAY_BUFFER, offset, sizeof(float), pos_SubD);
...
// В нужный момент этот VBO можно удалить
glDeleteBuffers(1, &vboId);