Готовый перевод OpenGL SuperBible (6th Edition) / OpenGL Супер Книга (6-ое издание): Data

Глава 5. Данные.
Что вы изучите в этой главе
Как создавать буферы и текстуры которые вы можете использоваться для хранения данных к которым получает доступ ваша программа
Как научить OpenGL передавать значение вершинных атрибутов автоматически
Как получить доступ к текстуре из шейдера для чтения и записи

В примерах, которые мы видели до сих пор, мы либо жёстко программировали данные в шейдерах либо мы передавали все значения за раз. Хотя этого достаточно для демонстрации работы конвейера OpenGL, вряд ли это показывает современное программирование графики. Современные графические процессоры спроектированы как потоковые процессоры которые принимают и вычисляют огромные массивы данных. Передача нескольких значений в OpenGL за один раз крайне неэффективно. Чтобы хранить данные и предоставить к ним доступ OpenGL мы вводим две основные формы хранения данных -- буферы и текстуры. В этой главе мы сначала введём буферы которые являются линейными блоками не типизированных данных которые можно считать generic memory allocations. Затем, мы введём текстуры которые обычно используются для хранения многомерных данных, таких как изображение или другие типы данных.

Буферы
В OpenGL буферы являются линейными данными ( linear allocations of memory ) которые можно использовать для различных целей. Они представлены именами которые являются непрозрачными ссылками ( opaque handles прим. пер. ) которые OpenGL использует для их идентификации. Прежде чем начать использовать буфер, вы должны попросить OpenGL зарезервировать имя для вас, затем использовать его для выделения памяти и для заполнения данными. Память выделяемая для буфера называется хранилищем данных.

Выделение памяти используя буферы
Функция которая используется для выделения памяти под буфер это glBufferData() прототип поторой

void glBufferData(GLenum target, GLsizeiptr size, const GLvoid * data, GLenum usage);

Параметр target говорит OpenGL под какой буфер вы хотите выделить память. Например, буфер который будет использоваться для хранения вершинных атрибутов называется GL_ARRAY_BUFFER. Возможно вы слышали термин вершинный буфер или юниформ буфер OpenGL, в отличии некоторых графических библиотек, на самом деле не назначает типы буферам -- буфер просто буфер и его можно использовать когда угодно и в любых целя ( и даже для нескольких задач если хотите ). Параметр size указывает OpenGL какого размера должен быть буфер и data является указателем на данные для буфера ( он может быть NULL если вы не хотите заполнять буфер сразу ). Наконец, usage говорит OpenGL как вы хотите использовать буфер. В Таблице 5.1 перечисленные возможные значения использования.

Таблица 5.1 Использование буфера
Листинг 5.1 показывает как резервировать имя с помощью glGenBuffers(), как присоединять к контексту используя glBingBuffer() и как выделяется под него вызовом glBufferData().

GLuint buffer;
//Создаём имя для буфера
glGenBuffers( 1, &buffer );
//Теперь присоединяем к контексту используя GL_ARRAY_BUFFER
glBingBuffer( GL_ARRAY_BUFFER, buffer );
//Укажем объём памяти который мы хотим использовать для буфера
glBufferData(GL_ARRAY_BUFFER, 1024 * 1024, NULL, GL_STATIC_DRAW);

После исполнения кода в Листинге 5.1, буфер содержит имя объекта буфера который был инициализирован для хранения 1 Мб данных для данных которые мы выберем. Использование GL_ARRAY_BUFFER подсказывает OpenGL что мы собираем использовать этот буфер для хранения вершинных данных, но мы сможем взять этот буфер и использовать для других целей. Есть несколько способов передачи данных в буфер. Вы возможно заметили что мы передали в качестве указателя NULL третьим аргументом функции glBufferData() в Листинге 5.1. Если бы мы передали данные в качестве указателя, то они бы использовались для инициализации буфера. Другой путь передачи данных в буфер это дать их OpenGL и сказать чтобы он их скопировал их в буфер. Чтобы сделать это мы вызываем glBufferSubData(), передавая size данных мы хотим их переместить в буфер, offset это смещение в буфере куда мы хотим их переместить и указатель на данные в памяти которые переместятся в буфер.

void glBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, const GLvoid * data);

Листинг 5.2 показывает как мы можем поместить данные первоначально использовавшиеся в Листинге 3.1 в объект буфера, который является 1 шагом в автоматической подачи вершины шейдера в данных.

Щелкните здесь для просмотра кода
// Данные, которые мы поместим в объект буфер
static const float data[] =
{
0.25, -0.25, 0.5, 1.0,
-0.25, -0.25, 0.5, 1.0,
0.25, 0.25, 0.5, 1.0
};
// Помещаем данные в буфер с нулевым смещением
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(data), data);
Листинг 5.2: Обновление содержимого буфера с помощью glBufferSubData()

Другим методом получения данных в буфер может быть запрос у OpenGL указателя на область памяти, содержащую буфер и копирование туда данных.
Листинг 5.3 показывает, как это сделать с помощью функции glMapBuffer ().

Нажмите здесь, чтобы посмотреть код
// Данные, которые мы поместим в буфер
static const float data[] =
{
0.25, -0.25, 0.5, 1.0,
-0.25, -0.25, 0.5, 1.0,
0.25, 0.25, 0.5, 1.0
};
// Получаем указатель на место хранения буфера
void * ptr = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
// Копируем туда наши данные
memcpy(ptr, data, sizeof(data));
// Сообщаем OpenGL, что мы закончили работу с указателем
glUnmapBuffer(GL_ARRAY_BUFFER);
Листинг 5.3: Маппинг хранилища буфера с помощью glMapBuffer ()

Заполнение и копирование данных в буферы.
После выделения места для хранения буфера с использованием glBufferData(), одним из возможных следующих шагов может быть заполнение буфера известными данными. Используете ли вы начальный параметр данных glBufferData(), glBufferSubData () для того чтобы поместить исходные данные в буфер, или glMapBuffer(), чтобы получить указатель на место хранения данных буфера и заполнения его с помощью вашего приложения, вам нужно переписать весь буфер целиком. Если данные, которые нужно поместить в буфер является постоянной величиной, гораздо более эффективным вероятно будет вызвать glClearBufferSubData(), прототипом которой является

void glClearBufferSubData(GLenum target,
GLenum internalformat,
GLintptr offset,
GLsizeiptr size,
GLenum format,
GLenum type,
const void * data);

Функция glClearBufferSubData () принимает указатель на переменную, содержащую значения, которыми вы хотите заполнить буфер и, после преобразования его в формат, указанный в internalformat, повторяет его по всей области, отведенной для буфера, определенной с помощью указанных размера и смещения, измеряемых в байтах. Формат и тип говорят OpenGL о данных, на которые указывают данные. Формат может быть одним из GL_RED, GL_RG, GL_RGB или GL_RGBA, определяя, к примеру, 1-, 2-, 3- или 4-канальные данные. Между тем, тип должен представлять тип данных компонентов. Например, это может быть GL_UNSIGNED_BYTE или GL_FLOAT для определения беззнаковых байт или данных с плавающей точкой. Наиболее распространенные типы, поддерживаемые OpenGL и соответствующие им типы данных в С приведены в таблице 5.2.

После того, как ваши данные были отправлены в GPU, вполне возможно, вы можете захотите распределить данные между буферами или скопировать результаты из одного буфера в другой. OpenGL предоставляет простой в использовании способ сделать это. glCopyBufferSubData () позволяет вам указать, какие буферы задействованы, а также размер и смещение.

void glCopyBufferSubData(GLenum readtarget,
GLenum writetarget,
GLintptr readoffset,
GLintptr writeoffset,
GLsizeiptr size);

Для целей этой книги мы разработали простое определение объектного файла, который назвали .SBM-файл, который хранит необходимую информацию, не будучи ни слишком простыми, ни слишком избыточно-подробным. Полное описание формата приведено в Приложении B. Фреймворк sb6 также включает в себя загрузчик, вызываемый sb6::object. Чтобы загрузить объектный файл, создайте экземпляр sb6::object, и вызовите его функцию загрузки следующим образом:

sb6::object my_object;
my_object.load("filename.sbm");

В случае успеха модель будет загружена в экземпляр объекта sb6::object и вы сможете отрендерить его. Во время загрузки класс создаст и настроит объект массива вершин модели, а затем
настроит все атрибуты вершин, содержащихся в файле модели. Класс также включает функцию рендеринга, которая связывает объект массива вершин модели и вызовы соответствующих команд рисования. Для пример, вызов функции

my_object.render();

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

нажмите здесь, чтобы посмотреть код изображения
GLint glGetUniformLocation(GLuint program,
const GLchar* name

Uniform блоки
Со временем, шейдеры, которые вы будете писать станут очень сложными.
Некоторые из них будут запрашивать много константных данных, и пропускать всех их в шейдер, используя uniform’ы может стать довольно неэффективно.Если у вас много шейдеров в приложении, вам будет нужно установить uniform’ы для каждого шейдера, что означает множество вызовов разных вариантов функций glUniform*(). Вы должны будете также отслеживать, какие униформы изменены. В то время как некоторые могут потребовать только инициализации один раз для всего приложения, другие могут меняться для каждого объекта, а некоторые один раз для фрейма. Это означает, что Вы должны обновить один из двух различных наборов униформы в различных местах в Вашем приложении (делающим его более сложным в эксплуатации) или обновить все униформы все время (стоящий работу). Это означает, что Вы должны обновить один из двух различных наборов uniform в различных местах в Вашем приложении (делающим его более сложным в эксплуатации) или обновлять все uniform все время (эффективные затраты).

Чтобы облегчить стоимость всего запроса glUniform*(), выполните обновление большого набора униформы более простым, и будьте в состоянии легко разделить ряд униформы между различными программами, OpenGL позволяет Вам объединять группу униформы в однородный блок и хранить целый блок в буферном объекте. Буферный объект точно такой же, как и любой другой, который был описан ранее. Вы можете быстро установить целую группу uniform одним из двух способов, изменением привязки Вашего буфера или переписыванием содержания предельного буфера. Вы можете также оставить buffer bound пока Вы изменяете программы, и, новая программа будет видеть текущий набор значений uniform. Эту функциональность называют буфер равномерного объекта или UBO. Фактически, uniform, которые Вы использовали до сих пор остаются в блоке по умолчанию. Любая uniform, объявленная в глобальном контексте в шейдере, заканчивается в блоке uniform по умолчанию.
Вы не можете держать блок по умолчанию в буфере равномерного объекта; Вы должны создать один или несколько назначенных uniform блоков.

Чтобы объявить, что ряд uniform будет сохранен в буферном объекте, Вы должны использовать назначенный uniform блок в своем шейдере. Это походит на интерфейсные блоки, описанные в секции “Интерфейсные Блоки” в Главе 3, но это использует ключевой uniform вместо “в” или “вне”. Листинг 5.9 показывает, что код похож на шейдер.

Нажмите здесь, чтобы увидеть изображение кода
uniform TransformBlock
{
float scale; // Глобальный масштаб, который относиться ко всему
vec3 translation; // Трансляция в X, Y, и Z
float rotation[3]; // Поворот вокруг осей X, Y, Z
mat4 projection_matrix; // Применение универсальной проекционной матрицы
// после масштаба и поворота
} преобразование;
Листинг 5.9: Пример объявления uniform блока

Щелкните здесь, чтобы рассмотреть пример кода
#version 430 core
// 2D изображение to store head pointers
layout (binding = 0, r32ui) coherent uniform uimage2D head_pointer;
// Буфер хранения шейдера, содержащий приложенные фрагменты структуры элемент_списка
{
vec4 color;
float depth;
int facing;
uint next;
};
layout (binding = 0, std430) buffer list_item_block
{
list_item item[];
};
layout (location = 0) out vec4 color;
const uint max_fragments = 10;
void main(void)
{
uint frag_count = 0;
float depth_accum = 0.0;
ivec2 P = ivec2(gl_FragCoord.xy);
uint index = imageLoad(head_pointer, P).x;
while (index != 0xFFFFFFFF && frag_count < max_fragments)
{
list_item this_item = item[index];
if (this_item.facing != 0)
{
depth_accum -= this_item.depth;
}
else
{
depth_accum += this_item.depth;
}
index = this_item.next;
frag_count++;
}
depth_accum *= 3000.0;
color = vec4(depth_accum, depth_accum, depth_accum, 1.0);
}
Листинг 5.46: Traversing a linked list in a fragment shader
Результат рендеринга с шейдерами в листинге 5.45 и 5.46 показан на фигуре 5.12.

Таблица 5.13. Целевая совместимость представления структуры

Таблица 5.14. Совместимость формата представления структуры

Выводы
В этой главе Вы узнали о том, как OpenGL имеет дело с огромным количеством данных требуемых для графической визуализации.

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

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

который затронутый в широком масштабе параллельной природы современных графических процессоров.

Переведено на Нотабеноиде
http://notabenoid.org/book/54048/235209

Переводчики: tnike, alexm_notabenoid, mulderm, Lebedev_Ivan

akkuraten

Настройки

Готово:

8.77% КП = 1.0

Непереведённые фрагменты будут пропущены.