На протяжении большинства уроков по OpenGL мы широко использовали возможности OpenGL-буферов для хранения данных в графическом процессоре. На этом уроке мы кратко обсудим несколько альтернативных подходов к управлению буферами.
Предназначения буферов
Буфер в OpenGL — это объект, который управляет определенной частью памяти графического процессора. Мы определяем содержание буфера, когда связываем его с конкретным типом хранимых данных, тем самым определяя его предназначение/роль. Например, связав буфер с целевым типом GL_ARRAY_BUFFER
, мы получим буфер, исполняющий роль буфера массива вершин. Ссылки на подобные буферы хранятся внутри OpenGL, и уже, в зависимости от предназначения/роли буфера, OpenGL взаимодействует с ними соответствующим образом.
До этого момента заполнение памяти буфера происходило через вызов функции glBufferData(), которая выделяла часть памяти графического процессора и затем помещала данные в эту память. Если мы в качестве аргумента данных передадим NULL
, то функция выделит память, но заполнять её не станет. Это полезно, если мы хотим сначала зарезервировать определенный объем памяти, чтобы позже вернуться к этому буферу.
Вместо того, чтобы одним вызовом функции целиком заполнить весь буфер, у нас есть возможность вызвать функцию glBufferSubData(), заполнив лишь определенную часть буфера. Данная функция в качестве аргументов ожидает получить предназначение буфера, смещение, размер данных и фактические данные. С помощью смещения мы указываем то место, от которого следует начать заполнять буфер. Это позволяет нам вставлять/обновлять только определенные части памяти буфера. Обратите внимание, что буфер должен иметь достаточное количество выделенной памяти, поэтому нам нужно вызвать функцию glBufferData() до использования функции glBufferSubData():
1 |
glBufferSubData(GL_ARRAY_BUFFER, 24, sizeof(data), &data); // диапазон: [24, 24 + sizeof(data)] |
Еще один способ поместить данные в буфер — это запросить указатель на память буфера и самостоятельно скопировать данные в эту память. Вызов OpenGL-функции glMapBuffer() возвращает указатель на область памяти текущего связанного буфера:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
float data[] = { 0.5f, 1.0f, -0.35f [...] }; glBindBuffer(GL_ARRAY_BUFFER, buffer); // Получаем указатель void *ptr = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY); // Копируем в память данные memcpy(ptr, data, sizeof(data)); // Убеждаемся в том, что уведомили OpenGL о завершении работы с указателем glUnmapBuffer(GL_ARRAY_BUFFER); |
Вызов функции glUnmapBuffer() сигнализирует OpenGL о том, что мы закончили работать с указателем, и после этого он станет недействительным. Функция возвращает GL_TRUE
, если OpenGL удалось успешно поместить наши данные в буфер.
Использование функции glMapBuffer() будет полезно при непосредственном отображении данных в буфере, без их предварительного сохранения во временной памяти. Например, непосредственное считывание данных из файла и копирование их в память буфера.
Групповая обработка вершинных атрибутов
Используя вызов функции glVertexAttribPointer(), мы указываем схему размещения атрибутов из буфера массива вершин. В буфере массива вершин атрибуты располагаются в чередующемся порядке, то есть координаты местоположения, нормали и/или текстурные координаты идут в памяти друг за другом (и так для каждой вершины). Теперь, когда мы знаем немного больше о буферах, мы можем использовать другой подход.
Например, то же самое можно было бы реализовать, если бы вместо чередования поместить все векторные данные в большие куски на каждый атрибут. Таким образом, вместо чередующегося размещения 123123123123
мы можем воспользоваться групповым подходом: 111122223333
.
При загрузке из файла данных вершин обычно извлекается массив координат местоположений объекта, массив нормалей и/или массив текстурных координат. Объединение этих массивов в один большой массив чередующихся данных может потребовать от программиста определенных дополнительных усилий. Таким образом, использование подхода группового размещения данных является более простым решением, которое мы можем легко реализовать с помощью функции glBufferSubData():
1 2 3 4 5 6 7 8 |
float positions[] = { ... }; float normals[] = { ... }; float tex[] = { ... }; // Заполняем буфер glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(positions), &positions); glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions), sizeof(normals), &normals); glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions) + sizeof(normals), sizeof(tex), &tex); |
В результате, мы можем непосредственно перенести массивы атрибутов в виде единого объекта прямо в буфер, не подвергая их предварительной обработке. Мы также могли бы объединить их в один большой массив и сразу же с помощью функции glBufferData() заполнить буфер, но для подобных задач идеально подходит функция glBufferSubData().
При этом необходимо обновить указатели атрибутов вершин, чтобы отразить вышеописанные изменения:
1 2 3 |
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), 0); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)(sizeof(positions))); glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)(sizeof(positions) + sizeof(normals))); |
Обратите внимание, что параметр stride
равен размеру атрибута вершины, так как следующий вектор атрибута вершины можно найти непосредственно после его трех (или двух) компонент.
Благодаря этому, мы получаем еще один подход к заданию атрибутов вершин. Стоит отметить, что вы вправе использовать любой из вышеописанных подходов. Прибегнув к групповому методу, вы получаете более организованный способ задания атрибутов вершин, но чередующийся вариант по-прежнему является рекомендуемым методом, поскольку после каждого запуска вершинного шейдера происходит выравнивание вершин.
Копирование буферов
Как только ваши буферы будут заполнены данными, вы можете поделиться этими данными с другими буферами или, возможно, скопировать содержимое одного буфера в другой буфер. Функция glCopyBufferSubData() позволяет нам с относительной легкостью копировать данные из одного буфера в другой. Прототип функции выглядит следующим образом:
1 2 |
void glCopyBufferSubData(GLenum readtarget, GLenum writetarget, GLintptr readoffset, GLintptr writeoffset, GLsizeiptr size); |
Параметр readtarget
определяет целевой объект считывания (из которого мы хотим скопировать данные), а параметр writetarget
— целевой объект записи (в который мы хотим скопировать данные). Мы могли бы, например, скопировать данные из буфера VERTEX_ARRAY_BUFFER
в буфер VERTEX_ELEMENT_ARRAY_BUFFER
, указав их, соответственно, в качестве целевых буферов чтения и записи. В результате этого указанные действия возымеют эффект на те буферы, которые в тот момент будут привязаны к соответствующим ролям.
Но что, если мы хотим читать и записывать данные в два разных буфера, которые оба являются буферами массива вершин? Мы не можем привязать два буфера одновременно к одному и тому же предназначению буфера. Именно поэтому OpenGL предоставляет нам еще два дополнительных предназначения для буферов: GL_COPY_READ_BUFFER
и GL_COPY_WRITE_BUFFER
. Далее мы связываем выбранные буферы с новыми предназначениями и указываем предназначения в качестве readtarget
и writetarget
аргументов.
Затем функция glCopyBufferSubData() считывает данные заданного размера size
от смещения readoffset
и записывает их в буфер writetarget
по смещению writeoffset
. Ниже показан пример копирования содержимого двух буферов массива вершин:
1 2 3 |
glBindBuffer(GL_COPY_READ_BUFFER, vbo1); glBindBuffer(GL_COPY_WRITE_BUFFER, vbo2); glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, 8 * sizeof(float)); |
Мы также могли бы реализовать его, связав буфер writetarget
с одним из новых предназначений буфера:
1 2 3 4 |
float vertexData[] = { ... }; glBindBuffer(GL_ARRAY_BUFFER, vbo1); glBindBuffer(GL_COPY_WRITE_BUFFER, vbo2); glCopyBufferSubData(GL_ARRAY_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, 8 * sizeof(float)); |
Имея некоторые дополнительные знания о манипулировании буферами, мы можем использовать их более эффективно. Чем дальше мы продвигаемся в OpenGL, тем более полезными становятся новые буферные методы. На следующем уроке мы обсудим uniform-буферы, где нам очень пригодится функция glBufferSubData().