OpenGL SuperBible (6th Edition) / OpenGL Супер Книга (6-ое издание): Следуя по конвейеру

← →
Английский оригинал Перевод на русский

Сожалеем, но текст оригинала доступен только зарегистрированным пользователям.

Глава 3. Следуя по конвейеру

Что нового вы узнаете в этой главе
• Что делает каждый этап конвейера
• Как соединить ваши шейдеры с этапами конвейера с фиксированными функциями.
• Как создать программу, которая одновременно использует каждый этап графического конвейера

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

alexm_notabenoid 14.08.14 в 08:31

0

Передача данных в Вершинный Шейдер
Вершинный ( или вертексный прим. пер.) шейдер является первой программируемой частью в конвейере OpenGL и отличается тем, что может быть единственной обязательной частью конвейера. Однако, прежде чем вершинный шейдер запустится fixed-function стадия известная как извлечение вершин или, иногда, вытягивание вершин действует. Это автоматически обеспечивание ввод в вершинный шейдер.

alexm_notabenoid 14.08.14 в 08:41

0

Атрибуты вершин
В GLSL( язык программирования шейдеров в OpenGL прим. пер. ) механизм для получения данных в шейдер и для возврата объявляется в глобальных переменных с ключевыми словами 'in' и 'out'. Вы были кратко введены в классификатор 'out' в Главе 2 где в Листинге 2.4 он использовался для вывода цвета из фрагментного шейдера. В начале конвейера OpenGL мы используем ключевое слово 'in' чтобы передать данные в вершинный шейдер. Между стадиями конвейера 'in и 'out' могут использоваться для передачи данных из шейдера в шейдер. Ну скоро вернёмся к этому. А теперь рассмотрим передачу данных в вершинный шейдер и что будет если вы объявите переменную с ключевым словом 'in'. Это обозначает переменную которая принимает данные из программы, а это значит что она автоматически заполнится на стадии fixed-function. Переменная станет атрибутом вершины.

Вершинные атрибуты показывают как вершинные данные входят в конвейер OpenGL. Чтобы объявить вершинный атрибут, объявите переменную в вершинном шейдере используя ключевое слово in. Пример этого показан в Листинге 3.1, где мы объявляем переменную offset в качестве входного атрибута.

alexm_notabenoid 14.08.14 в 08:49

0

#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: Объявление атрибута вершины

alexm_notabenoid 14.08.14 в 12:58

0

В Листинге 3.1 мы ввели переменную offset в качестве входящей переменной в вершинный шейдер. Т.к. это первый ввод данных в первый шейдер в конвейере он будет заполнен автоматически на стадии ввода вершинных атрибутов. Мы можем сказать это стадии какими данными заполнить переменные при помощи одной из множеств вариантов функции вершинных атрибутов ( glVertexAttrib*() ). Прототип функции для glVertexAttrib4fv(), которую мы будем использовать в этом примере, это

alexm_notabenoid 14.08.14 в 13:06

0

void glVertexAttrib4fv( GLuint index, const GLfloat *v );

alexm_notabenoid 14.08.14 в 13:07

0

Параметр index используется для ссылки на атрибут и v это указатель на новые данные которые будут помещены в атрибут. Вы возможно заметили layout ( location = 0 ) код в объявлении атрибута offset. Это ключевое слово layout и мы использовали его для того, чтобы установить месторасположение вершинного атрибута по адресу ( не такого как в Си прим. пер. ) ноль. Месторасположение это значение которое мы передаём в переменной index чтобы обратиться к атрибуту.

Каждый раз когда мы вызываем glVertexAttrib*() это обновляет значение атрибутов переданных в шейдер. Мы можем использовать для анимирование нашего треугольника. Листинг 3.2 показывает обновлённую версию нашей функции рисования которая обновления значение переменной offset каждый кадр.

alexm_notabenoid 14.08.14 в 13:42

0

// Наша функции рисования
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: Обновление вершинного атрибута

alexm_notabenoid 14.08.14 в 13:50

0

Когда мы запустим программу с функцией рисования из Листинга 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, мы можем передать значение из вершинного шейдера во фрагментный. Тогда, вместо вывода жёстко запрограммированного значения, фрагментный шейдер просто может вывести цвет переданный ему из вершинного шейдера.

alexm_notabenoid 14.08.14 в 15:04

0

#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: Вершинный шейдер с выходными параметрами

alexm_notabenoid 14.08.14 в 15:07

0

Как вы можете увидеть в Листинге 3.3, мы объявляем второй входной параметр в наш вершинный шейдер, color ( в этот раз по адресу 1 ), и записывает это значение в выходную переменную vs_output. Это подхватывает фрагментный шейдер в Листинге 3.4 и записывает во буфер кадра ( фреймбуфер прим. пер. ). Это позволяет передавать цвет на всём пути от вершинного атрибута, который мы можем установить при помощи glVertexAttrib*(), через вершинный шейдер во фрагментный и вывести его в буфер кадра, что означает что мы может рисовать разноцветные треугольники!

alexm_notabenoid 14.08.14 в 15:16

0

#version 430 core

// Вводим из вершинного шейдера
in vec4 vs_color;

// Вывод в буфер кадра
out vec4 color;
void main(void)
{
// Просто назначьте цвет который мы получили из вершинного шейдера
// в наш вывод
color = vs_color;
}

Листинг 3.4: Фрагментный шейдер с вводом

alexm_notabenoid 14.08.14 в 15:19

0

Блоки интерфейса

Объявление интерфейса переменных один из простых способов обмена данными между шейдерами. Тем не менее, в большинстве нетривильных программах, вы передавать множество различных данных между стадиями, и они могут содержать массивы, структуры и другие сложные механизмы переменных. Для достижения этого мы можем группировать переменные в блоке интерфейса. Объявление блока интерфейса похожа на структуру, кроме этого он объявляется вместе с ключевым словом in или out ввода или вывода из шейдера. Пример блока интерфейса определён показан в Листинге 3.5

alexm_notabenoid 14.08.14 в 16:29

0

#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: Вершинный шейдер с блоком интерфейса

alexm_notabenoid 14.08.14 в 16:31

0

Обратите внимание что блок интерфейса в Листинге 3.5 имеет имя блока (VS_OUT, в верхнем регистре) и имя экземпляра (vs_out, в нижнем регистре). Блоки передаются между шейдерами используя имя блока ( VS_OUT в этом регистре ), но для использования в шейдерах используются имена экземпляров. Таким образом изменяя фрагментный шейдер для использования блока интерфейса даёт нам такой код показанный в Листинге 3.6.

alexm_notabenoid 14.08.14 в 16:59

0

#version 430 core
// Объявление VS_OUT как выходной блок
in VS_OUT
{
vec4 color; // Отправим цвет на следующую стадию
} fs_in;
// Вывод в буфер кадра
out vec4 color;
void main(void)
{
// Просто назначьте цвет который мы получили из вершинного шейдера
// в наш вывод
color = fs_in.color;
}
Листинг3.6: Фрагментный шейдер со входным блоком интерфейса

alexm_notabenoid 14.08.14 в 17:01

0

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

vk955 24.08.14 в 06:08

0

Тесселяция

Тесселяция это процесс разбиения примитива ( который в OpenGL называется патч ) на множестве мелких таких как треугольники. OpenGL включает fixed-function настраиваемого движка тесселяции который способен разбить четырёхугольники, треугольники и линии во множество мелких точек, линий или треугольников которые могут быть обычно растеретизированны железом далее по всему конвейеру. Логично что фаза тесселяции идёт после вершинного шейдинга и она состоит из 3-ёх частей:
шейдер контроля тесселяции
fixed-function движок тесселяции
шейдер вычисления тесселяции.

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

Тесселяция в OpenGL работает разбивая большие поверхности, известные как патчи, в точки, линии или треугольники. Каждый патч формируется из числа контрольных точек. Номера контрольных точек каждого патча настраивается и устанавливается вызовом glPatchParameteri() с именем установленным в GL_PATCH_VERTICES и установленным значением контрольных точек которые будут использоваться каждый патч. Прототип glPatchParameteri() это

alexm_notabenoid 14.08.14 в 17:19

0

void glPatchParameteri( GLenum pname, GLint value );

alexm_notabenoid 14.08.14 в 17:45

0

По умолчанию, количество контрольных точек на патч равно трем, таким образом, если это то, что вы хотите (как в нашем примере), вам и вовсе не надо ее вызывать. Когда происходит тесселяция, вершинный шейдер выполняется по разу на каждую контрольную точку, в то время как шейдер контроля тесселяции выполняется партиями по группам точек контроля, где размер каждой партии равен количеству вершин на патч. То есть, вершины используются как контрольные точки, а результат работы вершинного шейдера передается партиями в шейдер контроля тесселяции в качестве входных данных. Количество контрольных точек на патч может быть изменено таким образом, что количество контрольных точек, которые выводятся из шейдера контроля тесселяции может быть отлично количества контрольных точек, которые им поглощаются. Количество контрольных точек произведенных шейдером управления устанавливается с помощью выходного спецификатора 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. Эта переменная содержит отсчитываемый от нуля индекс контрольной точки внутри патча, которая обрабатывается текущим вызовом шейдера контроля тесселяции.

vk955 24.08.14 в 06:11

0

#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: Наш первый шейдер контроля тесселяции

vk955 24.08.14 в 06:11

0

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

vk955 24.08.14 в 06:15

0

Шейдеры вычисления тесселяции
Завершив работу, функционально-фиксированный движок тесселяции породил ряд выходных вершин, представляющих сгенерированные им примитивы. Они передаются в шейдер вычисления тесселяции. Шейдер вычисления тесселяции (шейдер вычисления или сокращенно TES) вызовается для каждой вершины, произведенной тесселятором. Когда уровни тесселяции высокие, это значит, что шейдер вычисления тесселяции может быть вызван очень большое число раз, и поэтому вы должны быть осторожны со сложными шейдерами вычисления и высокими уровнями тесселяции.

Листинг 3.8 показывает шейдер вычисления тесселяции, который принимает входные вершины, произведенные тесселятором, как результат выполнения шейдера управления, показанного в листинге 3.7. В начале шейдера находится спецификатор layout, который устанавливает режим тесселяции. В данном случае, мы выбрали режим для треугольников. Другие спецификаторы, equal_spacing и cw, указывают, что новые вершины должна быть сгенерированы с равными интервалами вдоль ребер тесселированного полигона и что для сгенерированных треугольников должно быть использовано соединение вершин по часовой стрелке. Мы обсудим другие возможные варианты в разделе “Тесселяция” главы 8.

Далее в шейдере, вы можете видеть, что присваивается значение gl_Position, также как и в вершинном шейдере. Шейдер вычисляет это значение, используя содержимое еще предопределенных переменных. Второе - переменная-член gl_Position массива структур gl_in. Он совпадает со структурой gl_out, в которую производилась запись, ранее в шейдере управления тесселяцией из листинга 3.7. Этот шейдер по существу реализует сквозную тесселяцию. То есть, тесселированный выходной патч остается (точно такой же поверхностью / точно такой же формы) как и исходный, входной треугольный патч.

vk955 24.08.14 в 06:21

0

#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: Наш первый шейдер вычисления тесселяции

vk955 24.08.14 в 06:21

0

Чтобы увидеть результаты работы тесселятора, нам надо указать OpenGL рисовать только очертания результирующих треугольников. Чтобы это сделать, мы вызываем функцию glPolygonMode, чей прототип выглядит так:

vk955 24.08.14 в 06:23

0

void glPolygonMode(GLenum face,
GLenum mode);

vk955 24.08.14 в 06:23

0

Параметр face задает тип полигонов, на который мы хотим повлиять, и так как мы хотим повлиять на все, мы устанавливаем значение в GL_FRONT_AND_BACK. Другие режимы будут описаны в ближайшее время. Параметр mode задает то, как мы хотим отрисовывать наши полигоны. Так как мы хотим отрисовывать только каркас (т.е. линии), мы устанавливаем значение в GL_LINE. Результат рендеринга нашего единственного треугольника с включенной тесселяцией и двумя шейдерами из листинга 3.7 и 3.8 показан на рисунке 3.1.

vk955 24.08.14 в 06:22

0

Рисунок 3.1: Наш первый тесселированный треугольник

vk955 24.08.14 в 06:26

0

Геометрические шейдеры
Геометрический шейдер - это логически последний этап обработки шейдеров во front-end, находящийся после этапа обработки вершинного шейдера и этапа тесселяции и до растеризатора. Геометрический шейдер запускается по разу для каждый примитив и имеет доступ ко всем входным данным о всех вершинах, которые составляют обрабатываемый примитив. Геометрический шейдер, кроме того, уникален среди всех этапов обработки шейдеров тем, что способен увеличить или уменьшить количество данных, передаваемых через конвейер. Шейдеры тесселяции могут также увеличить или уменьшить количество работы, осуществляемой в конвейере, но только неявно, через установку уровня тесселяции для the патча. Геометрические шейдеры, с другой стороны, включают две функции - EmitVertex и EndPrimitive - которые явно порождают вершины, которые передаются на / для сборки примитивов и растеризации.

Другой уникальная способность геометрических шейдеров состоит в том, что они могут изменять режим примитивов посередине конвейера. Например, они могут принимать треугольники и создавать группы точек или линий в качестве выходных данных или даже треугольники из независимых точек. Пример геометрического шейдера показан в листинге 3.9.

vk955 24.08.14 в 06:31

0

#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: Наш первый геометрический шейдер.

vk955 24.08.14 в 06:32

0

Шейдер, показанный в листинге 3.9, действует как еще один сквозной шейдер, который преобразует треугольники в точки так, чтобы мы могли видеть их вершины. Первый спецификатор layout указывает, что геометрический шейдер ожидает в качестве входных данных увидеть треугольники. Второй спецификатор layout сообщает OpenGL, что геометрический шейдер будет создавать точки и что максимальное число точек, им создаваемое, равно трём. В главной функции, у нас есть цикл, который проходит через все члены массива gl_in, которые определяются путем вызова функции .length().

Мы на самом деле знаем, что размер массива будет равен трем, т.к. мы обрабатываем треугольники, и каждый треугольник имеет три вершины. Выходные данные геометрического шейдера также похожи на аналогичные данные вершинного шейдера. В частности, мы записываем значения в gl_Position, чтобы установить координаты результирующей вершины. Далее мы вызываем фунцию EmitVertex, которая создает вершину на выходе из геометрического шейдера. Геометрический шейдеры автоматически за вас вызывают функцию EndPrimitive в конце вашего шейдера, и поэтому в данном примере ее явный вызов необязателен. В качестве результата работы этого шейдера, будут созданы 3 вершины и они будут отрисованы в виде точек.

Если мы вставим этот геометрический шейдер в наш пример с простым единственным тесселированным треугольником, мы получим картинку как на рисунке 3.2. Чтобы создать эту картинку, мы устанавливаем размер точки равный 5.0 путем вызова функции glPointSize. Это делает точки большими и весьма заметными.

vk955 24.08.14 в 06:37

0

Рисунок 3.2: Тесселированный треугольник после добавления геометрического шейдера.

vk955 24.08.14 в 06:39

0

Сборка примитивов, отсечение и растеризация
После завершения front-end’а конвейера (который включает стадии обработки вершинного шейдера, тесселяции и геометрического шейдера), наступает время время функционально-фиксированной части конвейера, которая выполняет серию задач, которые принимают представление нашей сцены в виде вершин и преобразуют его в серию пикселей, которые в свою очередь должны быть закрашены и выведены на экран. Первый шаг в этом процессе - сборка примитивов, которая заключается в группировке вершин в линии и треугольники. Сборка примитивов задействуется также и для точек, но для них она тривиальна. Как только примитивы были составлены из их индивидуальных вершин, они отсекаются по отношению к отображаемой области, которая обычно является окном или экраном, но может быть и меньшей областью, известной как область просмотра (viewport). Наконец, части примитива, которые определены как потенциально видимые, передаются в функционально-фиксированную подсистему, называемую растеризатором. Этот блок определяет, какой пиксель перекрыт примитивом (точкой, линией или треугольником), и передает набор пикселей в следующую стадию - стадию обработки фрагментного шейдера.

vk955 24.08.14 в 06:41

0

Отсечение
Как только вершины покидают вершинный шейдер, их координаты, как говорят, должны быть в пространстве отсечения. Это одна из многих координатных систем, которая может быть применена для представления координат. Вы возможно заметили, что переменная 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.

vk955 24.08.14 в 07:06

0

Трансформация области просмотра
После отсечения, все вершины вашей геометрии имеют координаты, которые лежат между −1.0 и 1.0 по осям x and y. Наряду с координатой z, лежащей между 0.0 и 1.0, эти координаты называются нормированными координатами устройства. Однако, окно в которое вы рисуете, имеет координаты, которые начинаются в (0, 0) нижнем левом углу и диапазон (w – 1, h – 1), где w и h есть ширина и высота окна в пикселях соответственно. Чтобы поместить вашу геометрию в окно, OpenGL применяет трансформацию области просмотра, которая применяет масштабирование и сдвиг к нормированным координатам устройства каждой вершины, чтобы поместить их в координаты окна. Применяемое масштабирование и смещение определяются границами области просмотра, которые вы можете установить, вызвав функции glViewport и glDepthRange.

Эта трансформация имеет следующую форму:

vk955 24.08.14 в 07:12

0
+1

Здесь xw, yw и zw - результирующие координаты вершины в пространстве окна, а xd, yd и zd - входящие координаты вершины в нормализованном пространстве устройства. px и py - ширина и высота области просмотра в пикселях, а n и f - расстояния ближней и дальней плоскости отсечения по оси z соответственно. И наконец ox, oy и oz начало точки координат области просмотра.

vk955 24.08.14 в 07:13

0

Отбор вершин
Перед тем, как треугольник будет обработан далее, он может при необходимости быть пропущен через стадию, называемую culling, которая определяет, повернут ли треугольник в сторону наблюдателя или от него, и решает, основываясь на результате этого вычисления, нужно ли на самом деле передавать его дальше и отрисовывать. Если треугольник повернут в сторону наблюдателя, то он рассматривается в качестве фронтального (front-facing); в противном случае говорят, что он обращенный (back-facing). Обращенные треугольники очень часто отбрасываются, потому что, когда объект замкнут, любой обращенный треугольник будет скрыт другим фронтальным треугольником.

Чтобы определить, является ли треугольник фронтальным или обращенным, OpenGL определит его signed area в пространстве окна. Один из способов определить площадь треугольника - это взять векторное произведение двух его ребер. Уравнение для этой операции такое:

vk955 24.08.14 в 07:25

0

Здесь 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 показывает это наглядно с целью иллюстрации.

vk955 24.08.14 в 07:29

0

Рисунок 3.3: Порядок соединения вершин “по часовой стрелке” (слева) и “против часовой стрелки” (справа).

vk955 24.08.14 в 07:29

0

Как только направление, в котором обращен треугольник, определено, OpenGL способен отбросить либо прямо направленные, либо обратно направленные, либо оба типа треугольников. По умолчанию, OpenGL будет отрисовывать все треугольники, независимо от того, в какую сторону они обращены. Для включения culling’а, вызовите glEnable с параметром cap, установленным в GL_CULL_FACE. Когда вы включили culling, OpenGL будет cull обратно направленные треугольники по умолчанию. Чтобы поменять тип треугольников, которые будут culled, вызовите glCullFace с тем какую сторону выбрать, установленным в GL_FRONT, GL_BACK или GL_FRONT_AND_BACK.

Так как точки и линии не имеют никакой геометрической[1] области, эти вычисления к ним не применяются, и они не могут быть culled на этом этапе.
-------------------
1. Очевидно, что раз они отображаются на экране, точки и линии все же имеют область. Иначе мы бы их не увидели. Однако, эта область искусственна и не могут быть вычислена непосредственно из их вершин.

vk955 24.08.14 в 08:37

0

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

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

vk955 24.08.14 в 08:39

0

Фрагментные шейдеры
Фрагментный[2] шейдер - последний программируемый блок обработки в графическом конвейере OpenGL. Эта стадия отвечает за определение цвета каждого фрагмента перед его передачей в буфер кадра для возможной компоновки в окно. После того, как растеризатор обработает примитив, он создает список фрагментов, которые должны быть закрашены, и передает его во фрагментный шейдер. Здесь происходит огромный рост количества работы в конвейере, так как каждый треугольник может породить сотни, тысячи или даже миллионы фрагментов.
---------------------
2. Термин “фрагментный” использован для того, чтобы описать элемент, который в конечном итоге может повлиять на окончательный цвет пикселя. Пиксель может иметь цвет, отличный от полученного при любом конкретном вызове пиксельного шейдера в силу ряда других эффектов, таких как проверка на глубину и проверка шаблона, смешивание или множественная выборка сглаживания, которые будут рассмотрены далее в книге.
----------------------
Листинг 2.4 ранее в главе 2 содержит исходный код нашего первого фрагментого шейдера. Он чрезвычайно прост, так как объявляет единственную выходную переменную и затем присваивает ей неизменяемое значение. В реальном приложении, фрагментный шейдер был бы, как правило, существенно более сложным и ответственным за выполнение вычислений, связанных с освещением, применением материалов и даже определением глубины фрагмента. В качестве входных данных во фрагментном шейдере доступны несколько предопределенных переменных, таких как gl_FragCoord, которая содержит позицию фрагмента в окне. Можно использовать эти переменные, чтобы создать уникальный цвет для каждого фрагмента.

Листинг 3.10 показывает шейдер, который получает свой выходной цвет из gl_FragCoord, а рисунок 3.4 показывает результат запуска нашей исходной программы с единственным треугольником вместе с этим шейдером.

vk955 24.08.14 в 08:50

0

Рисунок 3.4: Результат листинга 3.10

vk955 24.08.14 в 08:51

0

#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: Получение цвета фрагмента из его позиции

vk955 24.08.14 в 08:51

0

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

Переменная gl_FragCoord одна из предопределенных переменных, доступных во фрагментном шейдере. Однако, также как и с другими этапами обработки шейдеров, мы можем определить свои собственные входные данные во фрагментный шейдер, которые будут заполнены на основе выходных данных в зависимости от того, какой этап был последним перед растеризацией. Например, если у нас есть простая программа с одним вершинным и одним фрагментным шейдером, мы можем передать данные из фрагментного шейдера в вершинный.

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

vk955 24.08.14 в 09:02

0

#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: Веришнный шейдер с выходными параметрами

vk955 24.08.14 в 09:04

0

Как вы можете видеть в листинге 3.11, мы добавили второй константный массив, содержащий цвета и индексы, используя gl_VertexID, записав его содержимое в выходную переменную vs_color. Теперь, мы модифицируем наш простой фрагментный шейдер, чтобы включить соответствующий вход и записать его значение на выход, как показано в листинге 3.12.

vk955 24.08.14 в 09:05

0

#version 430 core

// "vs_color" - цвет, созданный вершинным шейдером
in vec4 vs_color;

out vec4 color;

void main(void)
{
color = vs_color;
}

Листинг 3.12: Получение цвета фрагмента из его позиции

vk955 24.08.14 в 09:06

0

Результат использования этой новой пары шейдеров показан на рисунке 3.5. Как вы можете видеть, цвет плавно изменяется по всему треугольнику.

vk955 24.08.14 в 09:06

0

Рисунок 3.5: Результат листинга 3.12

vk955 24.08.14 в 09:07

0
Минутку...

Следующая страница →