Готовый перевод OpenGL SuperBible (6th Edition) / OpenGL Супер Книга (6-ое издание): Следуя по конвейеру
Глава 3. Следуя по конвейеру
Что нового вы узнаете в этой главе
• Что делает каждый этап конвейера
• Как соединить ваши шейдеры с этапами конвейера с фиксированными функциями.
• Как создать программу, которая одновременно использует каждый этап графического конвейера
В этой главе, мы пройдемся вдоль и поперёк по графическому конвейеру OpenGL, обеспечивая пониманием каждого из этапов, в том числе с блоками фиксированных функций и программируемых шейдерных блоков. Вы уже прочитали введение в стадии вершинного и фрагментного шейдера. Тем не менее, приложение которое вы создали просто рисует один неподвижный треугольник. Если мы хотим рисовать (рендерить прим. пер.) что-то поинтереснее мы должны знать гораздо больше о конвейере и о всех тех вещах которые вы можете проделывать с ним. Эта глава знакомит с каждой частью конвейера, соединяет их друг с другом, и содержит пример шейдера для каждой стадии.
Передача данных в Вершинный Шейдер
Вершинный ( или вертексный прим. пер.) шейдер является первой программируемой частью в конвейере OpenGL и отличается тем, что может быть единственной обязательной частью конвейера. Однако, прежде чем вершинный шейдер запустится fixed-function стадия известная как извлечение вершин или, иногда, вытягивание вершин действует. Это автоматически обеспечивание ввод в вершинный шейдер.
Атрибуты вершин
В GLSL( язык программирования шейдеров в OpenGL прим. пер. ) механизм для получения данных в шейдер и для возврата объявляется в глобальных переменных с ключевыми словами 'in' и 'out'. Вы были кратко введены в классификатор 'out' в Главе 2 где в Листинге 2.4 он использовался для вывода цвета из фрагментного шейдера. В начале конвейера OpenGL мы используем ключевое слово 'in' чтобы передать данные в вершинный шейдер. Между стадиями конвейера 'in и 'out' могут использоваться для передачи данных из шейдера в шейдер. Ну скоро вернёмся к этому. А теперь рассмотрим передачу данных в вершинный шейдер и что будет если вы объявите переменную с ключевым словом 'in'. Это обозначает переменную которая принимает данные из программы, а это значит что она автоматически заполнится на стадии fixed-function. Переменная станет атрибутом вершины.
Вершинные атрибуты показывают как вершинные данные входят в конвейер OpenGL. Чтобы объявить вершинный атрибут, объявите переменную в вершинном шейдере используя ключевое слово in. Пример этого показан в Листинге 3.1, где мы объявляем переменную offset в качестве входного атрибута.
#version 430 core
// "offset" это входящий атрибут вершины
layout (location = 0) in vec4 offset;
void main(void)
{
const vec4 vertices[3] = vec4[3](vec4( 0.25, -0.25, 0.5, 1.0),
vec4(-0.25, -0.25, 0.5, 1.0),
vec4( 0.25, 0.25, 0.5, 1.0));
// Прибавляем "offset" к нашим вершинным координатам
gl_Position = vertices[gl_VertexID] + offset;
}
Листинг 3.1: Объявление атрибута вершины
В Листинге 3.1 мы ввели переменную offset в качестве входящей переменной в вершинный шейдер. Т.к. это первый ввод данных в первый шейдер в конвейере он будет заполнен автоматически на стадии ввода вершинных атрибутов. Мы можем сказать это стадии какими данными заполнить переменные при помощи одной из множеств вариантов функции вершинных атрибутов ( glVertexAttrib*() ). Прототип функции для glVertexAttrib4fv(), которую мы будем использовать в этом примере, это
void glVertexAttrib4fv( GLuint index, const GLfloat *v );
Параметр index используется для ссылки на атрибут и v это указатель на новые данные которые будут помещены в атрибут. Вы возможно заметили layout ( location = 0 ) код в объявлении атрибута offset. Это ключевое слово layout и мы использовали его для того, чтобы установить месторасположение вершинного атрибута по адресу ( не такого как в Си прим. пер. ) ноль. Месторасположение это значение которое мы передаём в переменной index чтобы обратиться к атрибуту.
Каждый раз когда мы вызываем glVertexAttrib*() это обновляет значение атрибутов переданных в шейдер. Мы можем использовать для анимирование нашего треугольника. Листинг 3.2 показывает обновлённую версию нашей функции рисования которая обновления значение переменной offset каждый кадр.
// Наша функции рисования
virtual void render(double currentTime)
{
const GLfloat color[] = { (float)sin(currentTime) * 0.5f + 0.5f,
(float)cos(currentTime) * 0.5f + 0.5f,
0.0f, 1.0f };
glClearBufferfv(GL_COLOR, 0, color);
//Используем программу которую мы создали раннее
glUseProgram(rendering_program);
GLfloat attrib[] = { (float)sin(currentTime) * 0.5f,
(float)cos(currentTime) * 0.6f,
0.0f, 0.0f };
// Обновляем значение атрибута по адресу 0
glVertexAttrib4fv(0, attrib);
// Рисуем один треугольник
glDrawArrays(GL_TRIANGLES, 0, 3);
}
Листинг 3.2: Обновление вершинного атрибута
Когда мы запустим программу с функцией рисования из Листинга 3.2, то треугольник будет двигаться по овалу в окне.
Передача Данных от Стадии к Стадии.
До сих пор вы видели как передавать данные в вершинный шейдер создавая вершинные атрибуты используя ключевое слово in, как обращаться с fixed-function блоками данных читая и записывая встроенные переменные такие как gl_VertexID и gl_Position, и как вывести данные из фрагментного шейдера используя ключевое слово out. Тем не менее также возможно отправить свои данные от шейдера к шейдеру используя те же ключевые слова ( in и out ). Так же, как вы использовали ключевое слово out во фрагментном шейдере для создания выходной переменной в которой будет записан цвет, вы можете создать out переменную в вершинном шейдере используя ключевое слово out. Всё, что вы запишите в выходные переменные в одном шейдере будет послано в одноимённые переменные объявленные с ключевым словом in в следующем шейдере на следующей стадии. Например, если ваш вершинный шейдер объявляет переменную с именем vs_color и ключевым словом out, то она совпадёт с переменной vs_color объявленной с ключевым словом in во фрагментном шейдере ( при отсутствии других этапом между этими шейдерами ).
Если мы изменим наш вершинный шейдер как показано в Листинге 3.3 добавив vs_color как выходную переменную и, соотвественно, изменив фрагментный шейдер добавив vs_color как входящую переменную как показано в Листинге 3.4, мы можем передать значение из вершинного шейдера во фрагментный. Тогда, вместо вывода жёстко запрограммированного значения, фрагментный шейдер просто может вывести цвет переданный ему из вершинного шейдера.
#version 430 core
// "offset" и "color" are входные вершины атрибутов
layout (location = 0) in vec4 offset;
layout (location = 1) in vec4 color;
// "vs_color" выходная переменная которая будет передана
//на следующую стадию
out vec4 vs_color;
void main(void)
{
const vec4 vertices[3] = vec4[3](vec4( 0.25, -0.25, 0.5, 1.0),
vec4(-0.25, -0.25, 0.5, 1.0),
vec4( 0.25, 0.25, 0.5, 1.0));
// Прибавляем "offset" к нашим вершинным координатам
gl_Position = vertices[gl_VertexID] + offset;
// Записываем фиксированное значение в vs_color
vs_color = color;
}
Листинг 3.3: Вершинный шейдер с выходными параметрами
Как вы можете увидеть в Листинге 3.3, мы объявляем второй входной параметр в наш вершинный шейдер, color ( в этот раз по адресу 1 ), и записывает это значение в выходную переменную vs_output. Это подхватывает фрагментный шейдер в Листинге 3.4 и записывает во буфер кадра ( фреймбуфер прим. пер. ). Это позволяет передавать цвет на всём пути от вершинного атрибута, который мы можем установить при помощи glVertexAttrib*(), через вершинный шейдер во фрагментный и вывести его в буфер кадра, что означает что мы может рисовать разноцветные треугольники!
#version 430 core
// Вводим из вершинного шейдера
in vec4 vs_color;
// Вывод в буфер кадра
out vec4 color;
void main(void)
{
// Просто назначьте цвет который мы получили из вершинного шейдера
// в наш вывод
color = vs_color;
}
Листинг 3.4: Фрагментный шейдер с вводом
Блоки интерфейса
Объявление интерфейса переменных один из простых способов обмена данными между шейдерами. Тем не менее, в большинстве нетривильных программах, вы передавать множество различных данных между стадиями, и они могут содержать массивы, структуры и другие сложные механизмы переменных. Для достижения этого мы можем группировать переменные в блоке интерфейса. Объявление блока интерфейса похожа на структуру, кроме этого он объявляется вместе с ключевым словом in или out ввода или вывода из шейдера. Пример блока интерфейса определён показан в Листинге 3.5
#version 430 core
// "offset" это входящий атрибут вершины
layout (location = 0) in vec4 offset;
layout (location = 1) in vec4 color;
// Объявление VS_OUT как выходной блок
out VS_OUT
{
vec4 color; // Отправим цвет на следующую стадию
} vs_out;
void main(void)
{
const vec4 vertices[3] = vec4[3](vec4( 0.25, -0.25, 0.5, 1.0),
vec4(-0.25, -0.25, 0.5, 1.0),
vec4( 0.25, 0.25, 0.5, 1.0));
//Прибавляем "offset" к нашим вершинным координатам
gl_Position = vertices[gl_VertexID] + offset;
// Записываем фиксированное значение в vs_color
vs_out.color = color;
}
Листинг 3.5: Вершинный шейдер с блоком интерфейса
Обратите внимание что блок интерфейса в Листинге 3.5 имеет имя блока (VS_OUT, в верхнем регистре) и имя экземпляра (vs_out, в нижнем регистре). Блоки передаются между шейдерами используя имя блока ( VS_OUT в этом регистре ), но для использования в шейдерах используются имена экземпляров. Таким образом изменяя фрагментный шейдер для использования блока интерфейса даёт нам такой код показанный в Листинге 3.6.
#version 430 core
// Объявление VS_OUT как выходной блок
in VS_OUT
{
vec4 color; // Отправим цвет на следующую стадию
} fs_in;
// Вывод в буфер кадра
out vec4 color;
void main(void)
{
// Просто назначьте цвет который мы получили из вершинного шейдера
// в наш вывод
color = fs_in.color;
}
Листинг3.6: Фрагментный шейдер со входным блоком интерфейса
Связывание интерфейсных блоков между этапами через имя блока и возможность в каждом из них использовать разные имена для экземпляров служат двум целям: во-первых, это позволяет в каждом этапе изменить имя, с помощью которого вы ссылаетесь на блок, и во-вторых, позволяет интерфейсам быть не только одно элементными, но и массивами при переходе между определенными этапами, например этапами обработки вершинных шейдеров и этапом тесселяции или геометрических шейдеров, как мы увидим в скором времени. Заметьте, что интерфейсные блоки служат только для передачи данных от одного этапа обработки шейдера до другого - вы не можете использовать их, чтобы сгруппировать вместе входные данные в вершинный шейдер или выходные данные из фрагментного шейдера
Тесселяция
Тесселяция это процесс разбиения примитива ( который в OpenGL называется патч ) на множестве мелких таких как треугольники. OpenGL включает fixed-function настраиваемого движка тесселяции который способен разбить четырёхугольники, треугольники и линии во множество мелких точек, линий или треугольников которые могут быть обычно растеретизированны железом далее по всему конвейеру. Логично что фаза тесселяции идёт после вершинного шейдинга и она состоит из 3-ёх частей:
шейдер контроля тесселяции
fixed-function движок тесселяции
шейдер вычисления тесселяции.
Шейдер контроля тесселяции
Первая из 3-ёх фаз тесселяции это шейдер контроля тесселяции.
Этот шейдер принимает данные из вершинного шейдера и в первую очередь отвечает за две вещи:
Во-первых, определение уровня тесселяции который будет отправлен в движок тесселяции.
Во-вторых, генерация данных которые будут отправлены в шейдер вычисления тесселяции который запускатся после вычисления тесселяции ( видимо имеется ввиду не первая фаза шейдера, исправить прим. пер. ).
Тесселяция в OpenGL работает разбивая большие поверхности, известные как патчи, в точки, линии или треугольники. Каждый патч формируется из числа контрольных точек. Номера контрольных точек каждого патча настраивается и устанавливается вызовом glPatchParameteri() с именем установленным в GL_PATCH_VERTICES и установленным значением контрольных точек которые будут использоваться каждый патч. Прототип glPatchParameteri() это
void glPatchParameteri( GLenum pname, GLint value );
По умолчанию, количество контрольных точек на патч равно трем, таким образом, если это то, что вы хотите (как в нашем примере), вам и вовсе не надо ее вызывать. Когда происходит тесселяция, вершинный шейдер выполняется по разу на каждую контрольную точку, в то время как шейдер контроля тесселяции выполняется партиями по группам точек контроля, где размер каждой партии равен количеству вершин на патч. То есть, вершины используются как контрольные точки, а результат работы вершинного шейдера передается партиями в шейдер контроля тесселяции в качестве входных данных. Количество контрольных точек на патч может быть изменено таким образом, что количество контрольных точек, которые выводятся из шейдера контроля тесселяции может быть отлично количества контрольных точек, которые им поглощаются. Количество контрольных точек произведенных шейдером управления устанавливается с помощью выходного спецификатора layout в исходном коде шейдера управления. Такой спецификатор layout выглядит так:
layout (vertices = N) out;
Здесь N - количество контрольных точек на патч. Шейдер управления в ответе за расчет значений выходных контрольных точек и установку factors тесселяции для результирующего патча, который будет передан fixed-function движок тесселяции. Выходные factors тесселяции записываются в предопределенные выходные переменные gl_TessLevelInner и gl_TessLevelOuter, в то время как любые другие данные, которые передаются дальше по конвейеру записываются как обычно в определяемые пользователем выходные переменные (те, которые объявлены, используя ключевое слово out или специальный предопределенный массив gl_out). Листинг 3.7 показывает пример простого шейдера управления тесселяцией. Он устанавливает количество выходных контрольных точек равным трем (такое же, как и количество входных контрольных точек по умолчанию), используя спецификатор layout “layout (vertices = 3) out”, копирует значения своих входных переменных в выходные (используя предопределенные переменные gl_in и gl_out) и устанавливает внутренний и внешний уровень тесселяции равным 5. Предопределенная входная переменная gl_InvocationID используется для индексирования в массивах gl_in и gl_out. Эта переменная содержит отсчитываемый от нуля индекс контрольной точки внутри патча, которая обрабатывается текущим вызовом шейдера контроля тесселяции.
#version 430 core
layout (vertices = 3) out;
void main(void)
{
if (gl_InvocationID == 0)
{
gl_TessLevelInner[0] = 5.0;
gl_TessLevelOuter[0] = 5.0;
gl_TessLevelOuter[1] = 5.0;
gl_TessLevelOuter[2] = 5.0;
}
gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
}
Листинг 3.7: Наш первый шейдер контроля тесселяции
Движок тесселяции
Движок тесселяции - это функционально-фиксированная часть конвейера OpenGL, которая принимает сложноорганизованные поверхности, представленные в виде патчей, и разбивает их на более простые примитивы, такие как точки, линии и треугольники. До того, как движок тесселяции получает патч, шейдер контроля тесселяции обрабатывает входящие контрольные точки и устанавливает factors тесселяции, которые используются для разбивки патча. После того как движок тесселяции порождает выходные примитивы, вершины, представляющии их, забираются шейдером вычисления тесселяции. Движок тесселяции отвечает за порождение параметров, которые поступают в вызовы шейдера вычисления тесселяции, который далее используют их для трансформации результирующих примитивов и подготовку их для растеризации.
Шейдеры вычисления тесселяции
Завершив работу, функционально-фиксированный движок тесселяции породил ряд выходных вершин, представляющих сгенерированные им примитивы. Они передаются в шейдер вычисления тесселяции. Шейдер вычисления тесселяции (шейдер вычисления или сокращенно TES) вызовается для каждой вершины, произведенной тесселятором. Когда уровни тесселяции высокие, это значит, что шейдер вычисления тесселяции может быть вызван очень большое число раз, и поэтому вы должны быть осторожны со сложными шейдерами вычисления и высокими уровнями тесселяции.
Листинг 3.8 показывает шейдер вычисления тесселяции, который принимает входные вершины, произведенные тесселятором, как результат выполнения шейдера управления, показанного в листинге 3.7. В начале шейдера находится спецификатор layout, который устанавливает режим тесселяции. В данном случае, мы выбрали режим для треугольников. Другие спецификаторы, equal_spacing и cw, указывают, что новые вершины должна быть сгенерированы с равными интервалами вдоль ребер тесселированного полигона и что для сгенерированных треугольников должно быть использовано соединение вершин по часовой стрелке. Мы обсудим другие возможные варианты в разделе “Тесселяция” главы 8.
Далее в шейдере, вы можете видеть, что присваивается значение gl_Position, также как и в вершинном шейдере. Шейдер вычисляет это значение, используя содержимое еще предопределенных переменных. Второе - переменная-член gl_Position массива структур gl_in. Он совпадает со структурой gl_out, в которую производилась запись, ранее в шейдере управления тесселяцией из листинга 3.7. Этот шейдер по существу реализует сквозную тесселяцию. То есть, тесселированный выходной патч остается (точно такой же поверхностью / точно такой же формы) как и исходный, входной треугольный патч.
#version 430 core
layout (triangles, equal_spacing, cw) in;
void main(void)
{
gl_Position = (gl_TessCoord.x * gl_in[0].gl_Position +
gl_TessCoord.y * gl_in[1].gl_Position +
gl_TessCoord.z * gl_in[2].gl_Position);
}
Листинг 3.8: Наш первый шейдер вычисления тесселяции
Чтобы увидеть результаты работы тесселятора, нам надо указать OpenGL рисовать только очертания результирующих треугольников. Чтобы это сделать, мы вызываем функцию glPolygonMode, чей прототип выглядит так:
void glPolygonMode(GLenum face,
GLenum mode);
Параметр face задает тип полигонов, на который мы хотим повлиять, и так как мы хотим повлиять на все, мы устанавливаем значение в GL_FRONT_AND_BACK. Другие режимы будут описаны в ближайшее время. Параметр mode задает то, как мы хотим отрисовывать наши полигоны. Так как мы хотим отрисовывать только каркас (т.е. линии), мы устанавливаем значение в GL_LINE. Результат рендеринга нашего единственного треугольника с включенной тесселяцией и двумя шейдерами из листинга 3.7 и 3.8 показан на рисунке 3.1.
Рисунок 3.1: Наш первый тесселированный треугольник
Геометрические шейдеры
Геометрический шейдер - это логически последний этап обработки шейдеров во front-end, находящийся после этапа обработки вершинного шейдера и этапа тесселяции и до растеризатора. Геометрический шейдер запускается по разу для каждый примитив и имеет доступ ко всем входным данным о всех вершинах, которые составляют обрабатываемый примитив. Геометрический шейдер, кроме того, уникален среди всех этапов обработки шейдеров тем, что способен увеличить или уменьшить количество данных, передаваемых через конвейер. Шейдеры тесселяции могут также увеличить или уменьшить количество работы, осуществляемой в конвейере, но только неявно, через установку уровня тесселяции для the патча. Геометрические шейдеры, с другой стороны, включают две функции - EmitVertex и EndPrimitive - которые явно порождают вершины, которые передаются на / для сборки примитивов и растеризации.
Другой уникальная способность геометрических шейдеров состоит в том, что они могут изменять режим примитивов посередине конвейера. Например, они могут принимать треугольники и создавать группы точек или линий в качестве выходных данных или даже треугольники из независимых точек. Пример геометрического шейдера показан в листинге 3.9.
#version 430 core
layout (triangles) in;
layout (points, max_vertices = 3) out;
void main(void)
{
int i;
for (i = 0; i < gl_in.length(); i++)
{
gl_Position = gl_in[i].gl_Position;
EmitVertex();
}
}
Листинг 3.9: Наш первый геометрический шейдер.
Шейдер, показанный в листинге 3.9, действует как еще один сквозной шейдер, который преобразует треугольники в точки так, чтобы мы могли видеть их вершины. Первый спецификатор layout указывает, что геометрический шейдер ожидает в качестве входных данных увидеть треугольники. Второй спецификатор layout сообщает OpenGL, что геометрический шейдер будет создавать точки и что максимальное число точек, им создаваемое, равно трём. В главной функции, у нас есть цикл, который проходит через все члены массива gl_in, которые определяются путем вызова функции .length().
Мы на самом деле знаем, что размер массива будет равен трем, т.к. мы обрабатываем треугольники, и каждый треугольник имеет три вершины. Выходные данные геометрического шейдера также похожи на аналогичные данные вершинного шейдера. В частности, мы записываем значения в gl_Position, чтобы установить координаты результирующей вершины. Далее мы вызываем фунцию EmitVertex, которая создает вершину на выходе из геометрического шейдера. Геометрический шейдеры автоматически за вас вызывают функцию EndPrimitive в конце вашего шейдера, и поэтому в данном примере ее явный вызов необязателен. В качестве результата работы этого шейдера, будут созданы 3 вершины и они будут отрисованы в виде точек.
Если мы вставим этот геометрический шейдер в наш пример с простым единственным тесселированным треугольником, мы получим картинку как на рисунке 3.2. Чтобы создать эту картинку, мы устанавливаем размер точки равный 5.0 путем вызова функции glPointSize. Это делает точки большими и весьма заметными.
Рисунок 3.2: Тесселированный треугольник после добавления геометрического шейдера.
Сборка примитивов, отсечение и растеризация
После завершения front-end’а конвейера (который включает стадии обработки вершинного шейдера, тесселяции и геометрического шейдера), наступает время время функционально-фиксированной части конвейера, которая выполняет серию задач, которые принимают представление нашей сцены в виде вершин и преобразуют его в серию пикселей, которые в свою очередь должны быть закрашены и выведены на экран. Первый шаг в этом процессе - сборка примитивов, которая заключается в группировке вершин в линии и треугольники. Сборка примитивов задействуется также и для точек, но для них она тривиальна. Как только примитивы были составлены из их индивидуальных вершин, они отсекаются по отношению к отображаемой области, которая обычно является окном или экраном, но может быть и меньшей областью, известной как область просмотра (viewport). Наконец, части примитива, которые определены как потенциально видимые, передаются в функционально-фиксированную подсистему, называемую растеризатором. Этот блок определяет, какой пиксель перекрыт примитивом (точкой, линией или треугольником), и передает набор пикселей в следующую стадию - стадию обработки фрагментного шейдера.
Отсечение
Как только вершины покидают вершинный шейдер, их координаты, как говорят, должны быть в пространстве отсечения. Это одна из многих координатных систем, которая может быть применена для представления координат. Вы возможно заметили, что переменная gl_Position, в которую мы записали нашу вершину, тесселяция, а геометрические шейдеры имеют тип vec4 и что координаты, которые мы создали, записав в него, все являются четырехкомпонентными векторами. Это то, что называется однородной координатой. Однородная система координат используется в проективной геометрии. Значительная часть математики оказывается проще в однородном координатном пространстве, чем в обычном декартовом. Однородные координаты имеют еще один компонент, в отличие от их эквивалентных декартовых координат, и именно поэтому наши трехмерный координатный вектор представлен как четырехкомпонентная переменная.
Хотя выходное значение front-end’а - четырехкомпонентная однородная координата, отсечение происходит в декартовом пространстве, и поэтому, чтобы преобразовать из однородных координат в декартовы, OpenGL выполняет перспективное деление (perspective division), которое является процессом деления всех четырех компонентов позиции на последний компонент, w. Во всех примерах мы до сих пор устанавливали компонент w переменной gl_Position равным 1.0, и поэтому данное деление никакого не давало эффекта. Когда в скором времени мы исследуем проективную геометрию, мы обсудим влияние от установки значения компонента w, отличного от единицы.
После проективного деления, результирующая позиция теперь находится в нормированном пространстве устройства. В OpenGL, видимая область нормированного пространства устройства - это объем, простирающийся от −1.0 до 1.0 по осям x и y и от 0.0 до 1.0 по оси z. Любая геометрия, находящаяся в этих пределах, может стать видимой пользователю, а все, что находится за пределами, должно быть отброшено. Шесть сторон этого объема формируется плоскостями в трехмерном пространстве. Так как плоскость делит координатное пространство на две части, объемы, возникающие на каждой стороне плоскости, называются полупространствами.
Перед передачей примитивов в следующую стадию, OpenGL выполняет отсечение, определяя, на которой из сторон каждой из этих плоскостей лежит вершина каждого примитива. Каждая плоскость фактически имеет внешнюю область и внутреннюю область. Если все вершины примитива лежат за пределами внутренней область любой из плоскостей, он отбрасывается целиком. Если все вершины примитива лежат во внутренней области всех плоскостей и, таким образом, внутри видимого объема, он передается дальше неизменным. Примитивы, видимые частично (что означает, что они пересекают одну из плоскостей), должны быть обработаны особо. Больше деталей этого процесса приведено в разделе “Отсечение” в главе 7.
Трансформация области просмотра
После отсечения, все вершины вашей геометрии имеют координаты, которые лежат между −1.0 и 1.0 по осям x and y. Наряду с координатой z, лежащей между 0.0 и 1.0, эти координаты называются нормированными координатами устройства. Однако, окно в которое вы рисуете, имеет координаты, которые начинаются в (0, 0) нижнем левом углу и диапазон (w – 1, h – 1), где w и h есть ширина и высота окна в пикселях соответственно. Чтобы поместить вашу геометрию в окно, OpenGL применяет трансформацию области просмотра, которая применяет масштабирование и сдвиг к нормированным координатам устройства каждой вершины, чтобы поместить их в координаты окна. Применяемое масштабирование и смещение определяются границами области просмотра, которые вы можете установить, вызвав функции glViewport и glDepthRange.
Эта трансформация имеет следующую форму:
Здесь xw, yw и zw - результирующие координаты вершины в пространстве окна, а xd, yd и zd - входящие координаты вершины в нормализованном пространстве устройства. px и py - ширина и высота области просмотра в пикселях, а n и f - расстояния ближней и дальней плоскости отсечения по оси z соответственно. И наконец ox, oy и oz начало точки координат области просмотра.
Отбор вершин
Перед тем, как треугольник будет обработан далее, он может при необходимости быть пропущен через стадию, называемую culling, которая определяет, повернут ли треугольник в сторону наблюдателя или от него, и решает, основываясь на результате этого вычисления, нужно ли на самом деле передавать его дальше и отрисовывать. Если треугольник повернут в сторону наблюдателя, то он рассматривается в качестве фронтального (front-facing); в противном случае говорят, что он обращенный (back-facing). Обращенные треугольники очень часто отбрасываются, потому что, когда объект замкнут, любой обращенный треугольник будет скрыт другим фронтальным треугольником.
Чтобы определить, является ли треугольник фронтальным или обращенным, OpenGL определит его signed area в пространстве окна. Один из способов определить площадь треугольника - это взять векторное произведение двух его ребер. Уравнение для этой операции такое:
Здесь xiw и yiw - координаты i-ой вершины треугольника в пространстве окна, а i ⊕ 1 есть (i + 1) mod 3. Если область положительна, тогда треугольник рассматривается как фронтальный, а если отрицательна - как обращенный. Смысл этого расчета может быть обращен путем вызова glFrontFace() со значением для параметра dir установленным в GL_CW или GL_CCW (где CW и CCW означают “по часовой стрелке” и “против часовой стрелки” соответственно). Это называется порядком соединения вершин треугольника, а термины “по часовой стрелке” или “против часовой стрелки” относятся к порядку, в котором вершины появляются в пространстве окна. По умолчанию, это состояние установлено в GL_CCW, указывающее, что треугольники, чьи вершины соединяются в порядке против часовой стрелки, рассматриваются как фронтальные, а чьи вершины соединяются в порядке по часовой стрелке - как обращенные. Если состояние установлено GL_CW, то a берется с отрицательным значением перед тем, как будет использовано в процессе culling’а. Рисунок 3.3 показывает это наглядно с целью иллюстрации.
Рисунок 3.3: Порядок соединения вершин “по часовой стрелке” (слева) и “против часовой стрелки” (справа).
Как только направление, в котором обращен треугольник, определено, OpenGL способен отбросить либо прямо направленные, либо обратно направленные, либо оба типа треугольников. По умолчанию, OpenGL будет отрисовывать все треугольники, независимо от того, в какую сторону они обращены. Для включения culling’а, вызовите glEnable с параметром cap, установленным в GL_CULL_FACE. Когда вы включили culling, OpenGL будет cull обратно направленные треугольники по умолчанию. Чтобы поменять тип треугольников, которые будут culled, вызовите glCullFace с тем какую сторону выбрать, установленным в GL_FRONT, GL_BACK или GL_FRONT_AND_BACK.
Так как точки и линии не имеют никакой геометрической[1] области, эти вычисления к ним не применяются, и они не могут быть culled на этом этапе.
-------------------
1. Очевидно, что раз они отображаются на экране, точки и линии все же имеют область. Иначе мы бы их не увидели. Однако, эта область искусственна и не могут быть вычислена непосредственно из их вершин.
Растеризация
Растеризация - процесс определения того, какой фрагмент может быть перекрыт примитивом, типа линии или треугольника. Есть огромное число алгоритмов для определения этого, но большинство OpenGL-систем останавливаются на методе для треугольников, основанном на полупространствах, так как он хорошо поддается реализации параллельных вычислений. По сути, OpenGL определяет ограничивающий прямоугольник для треугольника в оконных координатах и проверяет каждый фрагмент внутри, чтобы определить, находится ли он внутри или вне треугольника. Для этого, OpenGL рассматривает каждое из трех ребер треугольника как полупространство, разделяющее окно на две части.
Фрагменты, лежащие внутри всех трех ребер, рассматриваются как лежащие внутри треугольника, а фрагменты, лежащие за пределами хоть одного любого из трех ребер - как лежащие за пределами треугольника. Поскольку алгоритм, определяющий на какой стороне линии лежит точка, относительно прост и независим от чего либо, кроме позиций конечных точек линии и тестируемой точки, то одновременно может быть осуществлено множество проверок, обеспечивая тем самым возможность массового параллелизма.
Фрагментные шейдеры
Фрагментный[2] шейдер - последний программируемый блок обработки в графическом конвейере OpenGL. Эта стадия отвечает за определение цвета каждого фрагмента перед его передачей в буфер кадра для возможной компоновки в окно. После того, как растеризатор обработает примитив, он создает список фрагментов, которые должны быть закрашены, и передает его во фрагментный шейдер. Здесь происходит огромный рост количества работы в конвейере, так как каждый треугольник может породить сотни, тысячи или даже миллионы фрагментов.
---------------------
2. Термин “фрагментный” использован для того, чтобы описать элемент, который в конечном итоге может повлиять на окончательный цвет пикселя. Пиксель может иметь цвет, отличный от полученного при любом конкретном вызове пиксельного шейдера в силу ряда других эффектов, таких как проверка на глубину и проверка шаблона, смешивание или множественная выборка сглаживания, которые будут рассмотрены далее в книге.
----------------------
Листинг 2.4 ранее в главе 2 содержит исходный код нашего первого фрагментого шейдера. Он чрезвычайно прост, так как объявляет единственную выходную переменную и затем присваивает ей неизменяемое значение. В реальном приложении, фрагментный шейдер был бы, как правило, существенно более сложным и ответственным за выполнение вычислений, связанных с освещением, применением материалов и даже определением глубины фрагмента. В качестве входных данных во фрагментном шейдере доступны несколько предопределенных переменных, таких как gl_FragCoord, которая содержит позицию фрагмента в окне. Можно использовать эти переменные, чтобы создать уникальный цвет для каждого фрагмента.
Листинг 3.10 показывает шейдер, который получает свой выходной цвет из gl_FragCoord, а рисунок 3.4 показывает результат запуска нашей исходной программы с единственным треугольником вместе с этим шейдером.
Рисунок 3.4: Результат листинга 3.10
#version 430 core
out vec4 color;
void main(void)
{
color = vec4(sin(gl_FragCoord.x * 0.25) * 0.5 + 0.5,
cos(gl_FragCoord.y * 0.25) * 0.5 + 0.5,
sin(gl_FragCoord.x * 0.15) * cos(gl_FragCoord.y * 0.15),
1.0);
}
Листинг 3.10: Получение цвета фрагмента из его позиции
Как вы можете видеть, цвет каждого пикселя в рисунке 3.4 теперь есть функция его позиции, что создало простой выравниваемый по экрану узор. Это шейдер из листинга 3.10, что создал клетчатые узоры на выходе.
Переменная gl_FragCoord одна из предопределенных переменных, доступных во фрагментном шейдере. Однако, также как и с другими этапами обработки шейдеров, мы можем определить свои собственные входные данные во фрагментный шейдер, которые будут заполнены на основе выходных данных в зависимости от того, какой этап был последним перед растеризацией. Например, если у нас есть простая программа с одним вершинным и одним фрагментным шейдером, мы можем передать данные из фрагментного шейдера в вершинный.
Входные данные во фрагментном шейдере несколько отличны от входных данных в других этапах обработки шейдеров по той причине, что OpenGL интерполирует их значения для примитива, для которого осуществляется рендеринг в данный момент. Чтобы это продемонстрировать, мы возьмем вершинный шейдер из листинга 3.3 и модифицируем его так, чтобы присвоить другое, константный цвет каждой вершине, как показано в листинге 3.11.
#version 430 core
// "vs_color" - выходная переменная, значение которой будет передано следующему этапу обработки шейдеров stage
out vec4 vs_color;
void main(void)
{
const vec4 vertices[3] = vec4[3](vec4( 0.25, -0.25, 0.5, 1.0),
vec4(-0.25, -0.25, 0.5, 1.0),
vec4( 0.25, 0.25, 0.5, 1.0));
const vec4 colors[] = vec4[3](vec4( 1.0, 0.0, 0.0, 1.0),
vec4( 0.0, 1.0, 0.0, 1.0),
vec4( 0.0, 0.0, 1.0, 1.0));
// Прибавляем "offset" к нашим жестко закодированным координатам вершины
gl_Position = vertices[gl_VertexID] + offset;
// Выводим неизменное значение через переменную vs_color
vs_color = color[gl_VertexID];
}
Листинг 3.11: Веришнный шейдер с выходными параметрами
Как вы можете видеть в листинге 3.11, мы добавили второй константный массив, содержащий цвета и индексы, используя gl_VertexID, записав его содержимое в выходную переменную vs_color. Теперь, мы модифицируем наш простой фрагментный шейдер, чтобы включить соответствующий вход и записать его значение на выход, как показано в листинге 3.12.
#version 430 core
// "vs_color" - цвет, созданный вершинным шейдером
in vec4 vs_color;
out vec4 color;
void main(void)
{
color = vs_color;
}
Листинг 3.12: Получение цвета фрагмента из его позиции
Результат использования этой новой пары шейдеров показан на рисунке 3.5. Как вы можете видеть, цвет плавно изменяется по всему треугольнику.
Рисунок 3.5: Результат листинга 3.12
Операции с буфером кадра
Буфер кадра представляет собой последний этап графического конвейера OpenGL. Он может представлять отображаемое содержимое экрана или ряд дополнительных областей памяти, которые используются для хранения других значений для каждого пикселя, не связанных с цветом. На большинстве платформ, это означает окно, которое вы видите на экране (или возможно экран целиком, если приложение его перекрывает) и то, что оно подчиняется операционной системе (или оконной подсистеме, если точнее). Буфер кадра, предоставленный оконной подсистемой, также известен как буфер кадра по умолчанию, но возможно предоставить свой собственный, если вы хотите, например, отрисовывать вне экранные области. Состояние, удерживаемое буфером кадра, включает в себя, например, куда должны быть записаны данные, созданные вашим фрагментным шейдером, какой формат должен быть у данных и так далее. Это состояние хранится в объекте, называемом объектом буфера кадра. Еще одной частью буфера кадра является состояние пиксельных операций.
Пиксельные операции
После того, как фрагментный шейдер вывел данные, с фрагментом могут произойти разные события, прежде чем он будет отображен в окне, например, принадлежит ли он вообще окну. Каждое из этих событий может быть активировано или отключено в вашем приложении. Первое событие, которое может произойти, это ножничный тест, который проверяет фрагмент на принадлежность прямоугольнику, который вы можете задать. Если он находится внутри прямоугольника, то будет обработан далее, если за пределами - будет отброшен.
Далее идет проверка шаблона. Этот процесс заключается в сравнении значение ссылки, предоставленной вашим приложением, с содержимым буфер шаблона, который хранит одно[3] значение для каждого пикселя. Содержимое буфера шаблона не имеет особого семантического значения и может использоваться для любых целей.
-----------------------------
3. На самом деле, буфер кадра обладает возможностью хранить для каждого пикселя множество значений глубины, шаблона или цвета при использовании техники множественной выборки. Мы поговорим о ней чуть позже.
------------------------------
После завершения проверки шаблона, выполняется проверка на глубину. Проверка на глубину - это операция сравнения координаты z фрагмента и содержимого буфера глубины. Буфер глубины - это область в памяти, которая, как и буфер шаблона, является частью буфера кадра, обладает достаточным объемом памяти для одного значения на каждый пиксель и содержит глубину (которая связана с расстоянием от зрителя) каждого пикселя. Обычно, значения в буфере глубины изменяются в диапазоне от 0 до 1, где 0 - самая ближняя из возможных точек в буфере глубины, а 1 - самая дальняя. Чтобы определить, находится ли фрагмент ближе, чем другие фрагменты, которые уже были отрисованы в этом же месте, OpenGL может сравнить компоненту z координаты фрагмента в пространстве окна со значением, уже находящимся в буфере глубины, и если оно меньше, чем то, что уже в буфере, то фрагмент видим. Чувствительность этой проверки также может быть изменена. Например, вы можете спросить OpenGL пропускать фрагменты, чья z-координата больше, равна или не равна содержимому буфера глубины. Результат проверки глубины также влияет на то, что OpenGL делает с буфером шаблона.
Далее, цвет фрагмента передается либо стадии операции смешивания, либо стадии логических операций, в зависимости от того, значения какого типа хранит буфер кадра: с плавающей запятой, нормированные или целые. Если содержимое буфера кадра - значения с плавающей запятой или нормированные, то применяется смешивание. Смешивание - легко настраиваемая стадия в OpenGL, которая будет в деталях разобрана с соответствующем разделе. Если вкратце, OpenGL способен использовать широкий набор функций, чтобы взять компонент выходных данных вашего фрагментного шейдера и текущего содержимого буфера кадра и вычислить новые значения, которые записываются назад в буфер кадра. Если буфер кадра содержит ненормированные целые значения, то логические операции, такие как AND, OR или XOR, могут быть применены к выходным данным вашего шейдера и значению, уже находящемуся в буфере кадра, чтобы создать новое значение, которое будет записано назад в буфер кадра.
Вычислительные шейдеры
Первый раздел этой главы описывает графический конвейер в OpenGL. Однако, OpenGL также включает стадию вычислительных шейдеров, которую можно представить себе практически как отдельный конвейер, выполняемый независимо от других специализированных на графике стадий.
Вычислительные шейдеры - это способ получения вычислительных мощностей, выдаваемых графическим процессором в системе. В отличие от графико-ориентированных вершинного, геометрического, фрагментного шейдеров и шейдера тесселяции, вычислительные шейдеры можно рассматривать как специальный, одноступенчатый конвейер, работающий сам по себе. Каждый вызов вычислительного шейдера выполняет одну единицу работы, также известную как элемент работы, несколько которых объединены вместе в небольшие группы, называемые локальными рабочими группами. Коллекции этих рабочих групп могут быть переданы в вычислительный конвейер OpenGL для обработки. Вычислительный шейдер не имеет никаких предопределенных входных или выходных переменных, помимо нескольких, чтобы указать шейдеру, над каким элементом работать. Вся обработка, выполненная вычислительным шейдером, явно записана в память самим шейдером, вместо того, чтобы быть потребленной следующей стадией конвейера. Очень простой вычислительный шейдер показан в листинге 3.13.
#version 430 core
layout (local_size_x = 32, local_size_y = 32) in;
void main(void)
{
// Ничего не делает
}
Листинг 3.13: Простой ничего-не-делающий вычислительный шейдер
В других отноошениях вычислительные шейдеры ни чем не отличаются от других этапов обработки шейдеров в OpenGL. Чтобы его скомпилировать, вы создаете объект шейдера с типом GL_COMPUTE_SHADER, присоединяете к нему ваш GLSL-код с помощью функции glShaderSource, компилируете, используя glCompileShader, а потом присоединяете к программе, вызвав glAttachShader() и glLinkProgram(). Результат - объект программы со скомпилированным вычислительным шейдром внутри, который может быть запущен, чтобы выполнить для вас работу.
Шейдер из листинга 3.13 сообщает OpenGL, что размер локальной рабочей группы будет равен 32, по числу элементов работы, а затем переходит к выполнению работы, в данном случае никакой. Чтобы сделать вычислительный шейдер, который действительно делает что-то полезное, вам нужно будет узнать немного больше об OpenGL, и поэтому мы вернемся к этому вопросу позже.
Заключение
В этой главе мы совершили небольшое путешествие по графическому конвейеру OpenGL. Вы (очень) бегло рассмотрели каждый большой блок обработки шейдеров и создали программу, которая использует каждый из них, только лишь для того, чтобы сделать нечто не особо впечатляющее. Мы намеренно забыли упомянуть несколько полезных функций OpenGL с намерением, чтобы вы уже через как можно меньшее количество страниц смогли получить результат на экране. На протяжении следующих нескольких глав, вы изучите более фундаментальные основы компьютерной графики и OpenGL, а потом мы снова пробежим по конвейеру, погружаясь в детали описанного в разделах этой главы, и рассмотрим наконец те вещи, которые мы пропустили в этом анонсе возможностей OpenGL.
Внимание! Этот перевод, возможно, ещё не готов.
Его статус: перевод редактируется
Переведено на Нотабеноиде
http://notabenoid.org/book/54048/234499
Переводчики: vk955, alexm_notabenoid
Настройки
Готово:
