Pull to refresh

Learn OpenGL. Урок 4.7 — Продвинутая работа с данными

Reading time5 min
Views17K
image

Продвинутая работа с данными



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


Буфер в OpenGL — это объект, который управляет определенной частью памяти, и ничего больше. Мы придаем значение буферу, привязав его его к определенному целевому буферу. Буфер — это всего лишь буфер массива вершин, когда мы привязываем его к GL_ARRAY_BUFFER, но мы так же можем легко связать его с GL_ELEMENT_ARRAY_BUFFER. OpenGL хранит буфер для каждой цели и, основываясь на цели, обрабатывает буферы по-разному.

До сих пор мы заполняли память, управляемую объектами буфера, вызывая glBufferData, которая выделяет часть памяти и добавляет данные в эту память. Если бы мы передали NULL в качестве аргумента данных, функция выделила бы память и не заполнила бы ее. Это полезно, если мы хотим сначала зарезервировать конкретную часть памяти, а в будущем вернуться к этому буферу, чтобы постепенно заполнить его.

Вместо заполнения всего буфера одним вызовом функции, мы также можем заполнить конкретные области буфера вызовом glBufferSubData. Эта функция принимает в качестве аргументов целевой буфер, смещение, размер данных и сами данные. Что нового в этой функции, так это то, что мы можем задать смещение, которое указывает откуда мы хотим начать заполнять буфер. Это позволяет нам вставлять/обновлять только определенные части памяти буфера. Обратите внимание, что буфер должен иметь достаточно выделенной памяти, поэтому вызов функции glBufferData обязательно должен быть перед вызовом glBufferSubData.

glBufferSubData(GL_ARRAY_BUFFER, 24, sizeof(data), &data); // Область: [24, 24 + sizeof(data)]

Еще один способ передать данные в буфер — это узнать указатель на память буфера, и напрямую скопировать данные в буфер самостоятельно. По вызову функции glMapBuffer OpenGL возвращает указатель на память текущего связанного буфера, чтобы работать с ним:

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);

OpenGL знает, что мы закончили работу с указателем, как только мы сообщаем ему это с помощью функции glUnmapBuffer. После выполнения этой функции, указатель становится недействительным, и функция возвращает GL_TRUE, если OpenGL успешно разместил ваши данные в буфере.

Использование glMapBuffer полезно для прямого размещения данных в буфере без предварительного хранения во временной памяти.

Атрибуты сгруппированных вершин


Используя glVertexAttribPointer, мы могли указать расположение атрибутов содержимого буфера массива вершин. В буфере массива вершин мы чередовали атрибуты, т.е. мы разместили позицию, нормаль и/или текстурные координаты по порядку для каждой вершины. Теперь, когда мы знаем немного больше о буферах, мы можем использовать другой подход.

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

Когда вы загружаете вершинные данные из файла, вы обычно получаете массив позиций, массив нормалей и/или массив текстурных координат. Это может стоить некоторых усилий, соединить эти массивы в большой массив чередующихся данных. Использование пакетного подхода — это легкое решение, которое мы можем реализовать с помощью функции glBufferSubData:

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 отлично подходит для таких задач.

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

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 равен размеру атрибута вершины, так как следующий атрибут атрибута вершины можно найти непосредственно после его 3 (или 2) компонентов.

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

Копирование буферов


Когда ваши буферы заполнены вашими данными, вы можете захотеть поделиться этими данными с другими буферами или, возможно, скопировать содержимое буфера в другой. Функция glCopyBufferSubData позволяет копировать данные из одного буфера в другой с относительной легкостью. Прототип функции выглядит следующим образом:

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. Пример копирования содержимого двух буферов массивов вершин показан ниже:

float vertexData[] = { ... };
glBindBuffer(GL_COPY_READ_BUFFER, vbo1);
glBindBuffer(GL_COPY_WRITE_BUFFER, vbo2);
glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sizeof(vertexData));

Также мы могли бы сделать это привязав буфер writetarget к одному из новых типов целевых буферов:

float vertexData[] = { ... };
glBindBuffer(GL_ARRAY_BUFFER, vbo1);
glBindBuffer(GL_COPY_WRITE_BUFFER, vbo2);
glCopyBufferSubData(GL_ARRAY_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sizeof(vertexData));

Имея некоторые дополнительные знания о том, как манипулировать буферами, мы уже можем использовать их более интересными способами. По мере погружения в OpenGL, эти методы работы с буфферами станут более полезными. В следующем уроке, где мы обсудим Uniform Buffer Objects, нам таки пригодится функция glBufferSubData.
Tags:
Hubs:
+16
Comments5

Articles

Change theme settings