Уроки по OpenGL с сайта OGLDev


Урок 05 - Uniform переменные

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

В этом уроке мы наконец-то получим хоть какое-то движение на экране. Для этого мы используем комбинацию uniform-переменной, значение которой мы будем изменять каждый кадр, и ленивую функцию, которую нам предлагает GLUT. Смысл в том, в что GLUT не вызывает нашу функцию рендера регулярно, а только по необходимости. GLUT вызывает функцию рендера только после получения событий, таких как сворачивание окна или если окно перекрыто другим окном. Если же мы не получили изменений в плане окна после запуска приложения, то функция рендера будет вызвана лишь раз. Вы можете проверить это сами, добавив вызов printf в свою функцию рендера. Вы увидите вывод лишь раз и ещё каждый раз во время сворачивания/разворачивания окна. Использование функции обратного вызова для рендера из прошлых уроков теперь не подойдет, так как нам нужно менять значение переменной. Для этого мы используем ленивую функцию обратного вызова. Ленивая функция вызывается GLUT'ом даже если никаких событий не произошло. Вы можете использовать другую функцию для этого вызова, в ней, например, будут производиться подсчеты, такие как повременные обновления или даже вызывать в ней функцию рендера. В этом уроке мы будем обновлять значение переменной внутри функции рендера.

Прямиком к коду!

glutIdleFunc(RenderSceneCB);

Здесь мы указываем функцию рендера в качестве ленивой. Заметьте, что если вы хотите использовать ленивую функцию, то необходимо добавить в конце функции рендера вызов glutPostRedisplay(). Иначе ленивая функция будет вызываться вновь и вновь, а вот функция рендера - нет. glutPostRedisplay() показывает, что текущее окно должно быть отрисованно заново и в течении главного цикла GLUT функция рендера будет вызвана.

gScaleLocation = glGetUniformLocation(ShaderProgram, "gScale");
assert(gScaleLocation != 0xFFFFFFFF);

После линковки программы мы запрашиваем позицию uniform-переменной в программном объекте. Это ещё один пример, когда среда приложения C/C++ должна быть сопоставлена со средой шейдера. У вас нет прямого доступа к содержимому шейдера и нельзя напрямую обновлять его переменные. Во время компиляции шейдера, компилятор GLSL запоминает индексы каждой uniform-переменной. Во внутреннем представлении шейдеров внутри компилятора доступ к переменной осуществляется через индекс. Индекс можно получить с помощью функции glGetUniformLocation. Вы вызываете эту функцию с указателям на объект программы и именем переменной. Функция возвращает индекс или -1 в случае ошибки. Очень важна проверка на ошибки (как мы и сделали ниже), иначе обновления переменной не попадут в шейдер. Есть 2 основные причины ошибки у этой функции. Вы написали с ошибкой имя переменной или она была убрана компилятором с целью оптимизации. Если компилятор не обнаружит использования переменной, он без раздумий выбросит её. В этом случае glGetUniformLocation не даст результата.

static float Scale = 0.0f;
Scale += 0.001f;
glUniform1f(gScaleLocation, sinf(Scale));

Мы используем статическую переменную типа float, которую мы будем по-немного увеличивать каждый вызов функции рендера (вы возможно захотите изменить значения с 0.001 если программа работает слишком быстро или медленно). Реальное же значение, передаваемое в шейдер - это синус от 'Scale' переменной. Это создаёт красивый цикл между -1.0 и 1.0. Заметьте, что sinf() принимает параметр в радианах, не в градусах, хотя нам сейчас это не важно, нам просто нужны волны, генерируемые синусом. Результат sinf() передается в шейдер используя glUniform1f. OpenGL обеспечивает множество видов этой функции вида glUniform{1234}{if}. Вы можете использовать их для загрузки значений в 1D, 2D, 3D или 4D (основывается на числе, которое указывает 'glUniform') векторов целочисленного или float-типов (это 'i' или 'f' окончания). Так же есть функция, которая принимает адрес вектора как параметр, а так же специальные версии для матриц. Первый параметр функции - это индекс позиции, который мы получили используя glGetUniformLocation().

Теперь посмотрим на изменения, сделанные в вершинном шейдере (фрагментный остался без изменений).

uniform float gScale;

Здесь мы объявляем uniform-переменную в шейдере.

gl_Position = vec4(gScale * Position.x, gScale * Position.y, Position.z, 1.0);

Мы умножаем координаты позиции X и Y на число, получаемое из приложения каждый кадр. Можете объяснить почему верхний угол треугольника половину цикла направлен вниз?

powered byDisqus