Найти в Дзене
ZDG

OpenGL #5: Оптимизация, ещё VAO и uniform

Оглавление

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

Предыдущие части: Раскрашиваем вершины, Наш первый зелёный шейдер, Рисуем треугольник, Приступим, помолясь, OpenGL на пальцах

Начнём с небольшой оптимизации. Это позволит нам лучше понять, как взаимосвязаны буферы, VAO-объекты и параметры шейдеров. Постоянное повторение – основа закрепления материала.

Итак, наш треугольник был представлен тремя числами на вершину (X, Y, Z):

Можно заметить, что координату Z мы не используем – она всегда равна нулю. Следовательно, можно её не передавать. Изменим массив, оставив в нём только координаты X и Y:

-2

Теперь надо соответствующим образом изменить настройку буфера:

-3

Там, где раньше передавалось 9 чисел типа float, теперь передаётся 6.

Далее нужно перенастроить VAO-объект, изменив аналогичным образом размер параметра.

VAO-объекты

В прошлые разы мы уже рассматривали VAO-объекты, и я допустил некоторые неточности (они уже исправлены). Сейчас попытаюсь объяснить получше.

У нас есть буфер с данными и вершинный шейдер. Каждый раз на вход шейдера подаётся очередная вершина, которая берётся из буфера. У вершины есть свойства, такие как X, Y, Z, цвет и т.д. Набор таких свойств, подаваемый как один параметр, называется вершинным атрибутом.

Это значит, что перед подачей данные в буфере нужно нарубить на порции определённого размера. Как в примерах выше, это могут быть порции по 3 числа или по 2 числа.

Но OpenGL не знает, как надо рубить. Мы просто создали буфер и наполнили его данными.

VAO-объект (Vertex Array Object) является посредником между сырым буфером и параметрами шейдера.

В нём задаются настройки для каждого входящего параметра шейдера. Сколько у шейдера входящих параметров, столько настроек создаётся внутри одного VAO-объекта. И они имеют индексы от 0 и далее.

С помощью функции glVertexAttribPointer() мы связываем параметры шейдера с порциями в буфере:

-4

В данном случае мы создаём в VAO такую настройку: для параметра с индексом 0 вершинный атрибут будет размером 2 числа типа GL_FLOAT, их не надо нормализовывать, размер порции будет 0, смещение будет NULL.

Последние два параметра следует рассмотреть внимательней. Мы указали размер порции 0, но он не нулевой. Просто это значит, что OpenGL вычислит его автоматически. Это возможно только для плотно упакованных массивов, где между элементами нет лишних байт. Нетрудно догадаться, что в этом случае размер порции будет равен размеру самого атрибута, т.е. 2 * sizeof(float).

Второй параметр – это смещение атрибута внутри порции. Скажем, порция имеет длину 12 байт, а конкретный атрибут в этой порции начинается с 6-го байта. Этот вариант мы рассмотрим ниже.

Шейдер

Настроив буфер, перейдём к вершинному шейдеру. Нужно изменить тип входящего параметра VertexPosition на vec2, так как теперь в нём только 2 числа.

-5

А в выходящем параметре gl_Position мы расширяем его до требуемого vec4, дописывая координату Z.

Запускаем и видим, что ничего не изменилось, потому что и не должно:

-6

Но изнутри мы теперь передаём в буфер меньше чисел и дополняем отсутствующую координату в шейдере.

Один на всех

Теперь вспомним, что у нас есть отдельный от точек массив цветов, отдельный буфер для него, и мы также отдельно привязывали его через VAO к параметру шейдера с индексом 1.

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

Сведём вместе массив вершин и массив цветов:

-7

Теперь каждая вершина занимает 5 чисел: X, Y, R, G, B. Нам стал не нужен второй буфер, а вот первый опять подредактируем, ведь теперь туда надо закачать 15 чисел:

-8

Теперь настроим VAO. Для первого параметра шейдера всё остаётся по-старому, только меняем размер порции буфера на 5 * sizeof(float):

-9

Это значит: рубим буфер кусками по 5 чисел, но размер параметра с индексом 0 – два числа, и берётся он от начала куска.

Теперь настроим второй параметр с индексом 1:

-10

Это значит: рубим буфер кусками по 5 чисел, но размер параметра с индексом 1 – три числа, и берётся он со смещением в 2 числа от начала куска. Модификатор (void *) перед смещением нужен для того, чтобы преобразовать тип int в указатель. Это просто требование функции, так что можно не обращать внимания.

Итого: кусок буфера это 5 чисел. Первый параметр шейдера (с индексом 0) берёт первые два числа (X, Y), второй параметр шейдера (с индексом 1) берёт оставшиеся 3 числа (R, G, B).

Запускаем и видим, что ничего не изменилось, потому что и не должно:

-11

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

uniform

Не смог найти официального перевода на русский. "Одинаковый" – это один из модификаторов роли параметров шейдера. Как мы уже знаем, есть входные параметры (in), есть выходные (out). Есть также глобальные параметры, не входные и не выходные. Они просто есть, и обозначаются как uniform.

Отличие их в том, что in/out параметры читаются из буферов, пишутся в буфера и в каждый вызов шейдера у них получаются другие значения. А вот uniform существуют сами по себе, одинаковы для всех шейдеров и вызовов внутри одной программы, и не зависят ни от каких буферов.

В языке C это были бы глобальные переменные, и довольно странно, что их не назвали global. Или я опять чего-то не знаю.

Такие переменные нужны для обмена любой информацией между нашей программой и шейдером. Сейчас мы сделаем небольшой тест.

Пульсация цвета

Добавим в шейдер внешнее управление цветом, а точнее, его яркостью. Теперь он будет не просто отдавать на выход тот же самый цвет, а будет умножать его на некий параметр, который назовём "прогресс". То есть по мере того, как прогресс движется от 0 до 1, яркость цвета также будет меняться от 0 до 1.

Работа с uniform начинается с шейдера. Здесь всё наоборот – мы не засылаем из основной программы в OpenGL свои данные, а вытаскиваем их из OpengGL в основную программу.

Итак, дописываем в вершинный шейдер параметр progress:

-12

Он вещественного типа и будет меняться от 0 до 1, а затем сбрасываться обратно в ноль.

Чтобы получить выходной параметр Color, соответствующий прогрессу, нужно входной VertexColor умножить на progress.

Но так не очень интересно. Чтобы пульсация была плавной, мы добавляем в выражение функцию синуса. Синус совершает полный цикл от 0 до 2𝜋 – это будет прогресс, и принимает значения от -1 до 1, так что мы приводим его к диапазону 0..1, прибавляя 1 и деля на 2.

Всё это вычисляется в шейдере. Теперь задача – сделать так, чтобы мы меняли прогресс в основной программе, и он соответственно менялся бы в шейдере.

Нам нужно вытянуть местоположение переменной progress из OpenGL в свою программу. Отмечу, что это делается уже ПОСЛЕ того, как OpenGL-программа слинкована.

-13

Мы вызываем функцию glGetUniformLocation(), передавая туда идентификатор OpenGL-программы program и имя uniform-переменной, которое нас интересует: "progress".

В ответ мы получим некий целочисленный идентификатор (номерок), который связан с этой переменной. Мы сохранили его в своей переменной progress_loc. Отмечу, что никакой связи между переменной progress в шейдере и нашей переменной progress_loc нет. Мы получили только номерок переменной progress, а не указатель на неё.

Теперь заведём собственную переменную progress, которая будет меняться от 0 до 1. Опять же отмечу, что она никак не связана с переменной progress в шейдере. Это просто наша переменная.

Добавим в главный цикл изменение нашей переменной progress, чтобы она наращивалась долями по 0.01 и сбрасывалась в 0, когда достигнет 1:

-14

Теперь осталось передавать значение нашей переменной progress в шейдерную uniform-переменную progress:

glUniform1f(progress_loc, progress);

В функцию glUniform1f() мы передаём ранее добытый номерок uniform-переменной и значение нашей переменной progress.

Это приведёт к тому, что в uniform-переменную progress в шейдере запишется наше значение.

Финальный код:

-15

Если теперь запустить программу, мы увидим, что яркость плавно меняется от 0 до 1 и обратно. Потому что на каждом шаге цикла в шейдер попадает новое значение progress. Но показать это на картинке невозможно, так что вот просто картинка с яркостью около 0:

-16

Небольшое пояснение насчёт имени функции glUniform1f. Буквы 1f в конце обозначают, что мы хотим передать в uniform-переменную одно (1) значение типа float (f). Если бы передавали тип int, тогда функция называлась бы glUniform1i. Если бы 2 значения float, то называлась бы glUniform2f. И так далее. Думаю, что логика понятна. Легко понять, что функций glUniform** с различными окончаниями много, но все их можно найти в документации.

Код для данного выпуска лежит на github в ветке uniform.

Читайте дальше: Текстуры