Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
МУ к ЛР_v0.docx
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
587.09 Кб
Скачать

Вывод примитивов на экран

В предыдущих версиях OpenGL (младше 3.х) для того, чтобы нарисовать на экране объект, приходилось использовать конструкции вида:

glBegin(GL_TRIANGLES);

glColor3f(1.0f, 0.0f, 0.0f); //Красный цвет (пиксель)

glVertex3f(-1.0f, -1.0f, 0.0f); //Вершина

glColor3f(1.0f, 1.0f, 1.0f); //Белый цвет (пиксель)

glVertex3f(0.0f, 1.0f, 0.0f); //Вершина

glColor3f(0.0f, 0.0f, 1.0f); //Синий цвет (пиксель)

glVertex3f(1.0f, -1.0f, 0.0f); //Вершина

glEnd();

Данный фрагмент кода рисует на экране закрашенный треугольник с разноцветными вершинами.

Начиная с OpenGL версии 3.3 из стандарта был удален Fixed-Function Pipeline (FFP, фиксированный графический конвейер). Теперь такие данные, как вершины и пиксели, обрабатываются шейдером (от английского "shade" – тень) – специализированная часть программы, исполняемая непосредственно на графическом процессоре (GPU), и изначально служившая для реализации сложных графических эффектов посредством очень тонкой передачи игры света и тени [2].

Чтобы понять, что такое шейдер, для начала необходимо разобраться, как видеокарта рисует примитивы (треугольники, полигоны и др.): На вход поступают данные о каждой вершине примитива. Например, положение вершины в пространстве, нормаль и текстурные координаты. Эти данные называются вершинными атрибутами (vertex attributes). GPU на их основе вычисляет выходные значения: положение вершины в экранных координатах, цвет вершины, рассчитанный в зависимости от освещения и так далее.

До выхода OpenGL версии 2.0 этот процесс был неуправляемым. Приходилось довольствоваться возможностями, предоставленными самой видеокартой, либо выполнять расчеты для каждой вершины на центральном процессоре (CPU), что намного медленнее. Для решения этой проблемы были предложены шейдеры.

Важной особенностью шейдеров является то, что все инструкции работают с векторами. Например, чтобы посчитать скалярное произведение, надо выполнить всего одну инструкцию, а не 5 (2 сложения и 3 умножения), как на CPU. А если инструкций мало, то скорость выполнения такой программы довольно высокая.

Для возможности взаимодействия приложения с шейдерной программой также применяются юниформ переменные (Uniform Variables) – это данные, посылаемые в шейдер приложением; в отличие от вершинного атрибута они глобальны как для шейдеров, так и для вершин. То есть если объявить юниформ переменную с одинаковым именем в вершинном и фрагментом шейдерах, они будут общими и для них. Также юниформ переменные остаются неизменными ровно до тех пор, пока их не изменит приложение.

Упрощенная модель Programmable Pipeline (PP, программируемый графический конвейер) представлена на рисунке 3.

Рисунок 3 – Упрощенная модель графического конвейера OpenGL 3.3.

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

В стандарте OpenGL версии 3.3 шейдеры делятся на три типа:

– Вершинный шейдер (Vertex shader) оперирует данными, сопоставленными с вершинами многогранников. К таким данным, в частности, относятся координаты вершины в пространстве, текстурные координаты, тангенс-вектор, вектор бинормали, вектор нормали. Вершинный шейдер может быть использован для видового и перспективного преобразования вершин, генерации текстурных координат, расчета освещения. Обязательной работой является запись позиции вершины в переменную – gl_Position [3; 50].

– Геометрический шейдер (Geometry shader) в отличие от вершинного способен обработать не только одну вершину, но и целый примитив. Это может быть отрезок (две вершины) и треугольник (три вершины), а при наличии информации о смежных вершинах (adjacency) может быть обработано до шести вершин для треугольного примитива. Кроме того, геометрический шейдер способен генерировать новые примитивы, не задействуя при этом центральный процессор [3; 84].

– Фрагментный шейдер (Fragment shader) работает с фрагментами изображения. Под фрагментом изображения в данном случае понимается пиксель, которому поставлен в соответствие некоторый набор атрибутов, таких как цвет, глубина, текстурные координаты. Фрагментный шейдер используется на последней стадии графического конвейера для формирования фрагмента изображения [3; 185].

Набор, состоящий из нескольких шейдеров, называется шейдерной программой. Минимально необходимым набором, без которого в OpenGL версии 3.3 невозможно получить никакой картинки на экране, является набор, состоящий из вершинного и фрагментного шейдеров. Таким образом, первый шаг, который необходимо сделать, чтобы вывести какой-либо объект на экран, – это загрузка и подготовка шейдерной программы.

Загрузка и подготовка шейдеров

На ранних этапах для написания шейдеров использовались специализированные инструкции ассемблера. Когда количество инструкций в шейдерах достигло десятков, сотен, появился специализированный язык высокого уровня – GLSL (OpenGL Shading Language) [4]. Синтаксис языка базируется на языке программирования ANSI C, однако, из-за специфической направленности, из языка были исключены многие возможности для его упрощения и повышения производительности. А для возможности работы с векторами и матрицами в GLSL были добавлены необходимые функции и типы данных.

Рассмотрим простейший пример кода для вершинного шейдера:

#version 330 core

layout (location = 0) in vec2 inPosition;

layout (location = 1) in vec3 inColor;

smooth out vec3 currentColor;

void main() {

currentColor = inColor; //Передаем информацию о цвете в пиксельный шейдер

gl_Position = vec4(inPosition, 0.0, 1.0); //Задаем положение вершины

}

Входные атрибуты обозначаются идентификатором in (только для чтения), а выходные – out (только для записи). Шейдер читает из памяти видеокарты два атрибута – положение и цвет, сохраняет положение вершины во встроенную переменную gl_Position и с помощью выходной переменной currentColor передает значение цвета на следующий этап обработки, в данном случае – пиксельный шейдер:

#version 330 core

out vec4 outputColor;

smooth in vec3 currentColor;

void main() {

outputColor = vec4(currentColor, 1.0); //Задаем цвет пикселя

}

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

– smooth – перспективно-корректная интерполяция (по умолчанию);

– flat – без интерполяции (по правилу старшинства вершины);

– noperspective – линейная интерполяция.

Таким образом, в случае с атрибутом currentColor цвет меняется "линейно" от вершины к вершине.

Для лучшей переносимости шейдеров на различные аппаратные и программные платформы GLSL шейдеры принято хранить в виде исходных кодов. Исходные коды компилируются драйвером видеокарты. Драйвер генерирует оптимальный двоичный код, который понимает данное оборудование. Это гарантирует, что один и тот же шейдер будет правильно и эффективно работать на различных платформах.

Исходный код может быть представлен в виде ANSI строк, завершающихся знаком переноса строки ('\n') в конце или без него. В случае, если знака переноса нет, необходимо передать массив длин каждой строки.

Для того, чтобы подготовить шейдеры к работе и собрать из них шейдерную программу, необходимо произвести следующие шаги:

1. Создать уникальные имена под каждый тип шейдеров – glCreateShader, и шейдерную программу – glCreateProgram.

//Создаем программу

programID = glCreateProgram();

//Создаем шейдер

auto vertexShaderID = glCreateShader(GL_VERTEX_SHADER);

auto fragmentShaderID = glCreateShader(GL_FRAGMENT_SHADER);

2. На соответствующий тип шейдера (уникальное имя) загрузить его исходный код – glShaderSource.

GLchar* vertexShaderSource, fragmentShaderSource; //Исходные тексты шейдеров

//Указываем где хранится исходный текст шейдера

glShaderSource(vertexShaderID, 1, &vertexShaderSource, NULL);

glShaderSource(fragmentShaderID, 1, &fragmentShaderSource, NULL);

3. Скомпилировать каждый тип шейдера – glCompileShader.

//Производим компиляцию шейдера

glCompileShader(vertexShaderID);

glCompileShader(fragmentShaderID);

4. Прикрепить шейдеры к программе – glAttachShader.

//Ассоциируем шейдер с программой

glAttachShader(programID, vertexShaderID);

glAttachShader(programID, fragmentShaderID);

5. Произвести линковку шейдерной программы – glLinkProgram.

//Загружаем программу в память видеокарты

glLinkProgram(programID);

6. Установить шейдерную программу текущей – glUseProgram

//Делаем активной программной

glUseProgram(programID);

Для проверки состояния компиляции шейдера и линковки шейдерной программы, а также для получения размера сообщения об ошибке, используются команды: glGetShaderiv и glGetProgramiv соответственно.

Для получения сообщения об ошибках во время компиляции шейдера и линковки программы используются команды: glGetShaderInfoLog и glGetProgramInfoLog соответственно.

Для освобождения ресурсов, занятых шейдером и шейдерной программой, используются команды: glDetachShader, glDeleteShader и glDeleteProgram.

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

Вершинный буфер и его связь с вершинными атрибутами

Для хранения данных о вершинах геометрии в OpenGL существует специальный объект – Vertex Buffer Object (VBO, вершинный буфер). VBO позволяет создать буфер в памяти видеокарты и помещать туда необходимые данные. VBO создается с подсказкой, как часто будут меняться данные в этом буфере.

Стоит отметить, что создания одного VBO недостаточно, необходимо создать еще один специальный объект – Vertex Array Object (VAO, массив вершин). VAO предназначен для хранения связей между параметрами вершинных атрибутов и источниками данных в VBO. В VAO хранится информация о том, из какого VBO какие атрибуты необходимо прочитать, а также их тип, размер и необходимое смещение до начала данных. Однако, существует очень важное исключение – VBO для хранения индексов (GL_ELEMENT_ARRAY_BUFFER), – применяется для изменения порядка обхода вершин, к VAO возможно присоединить только один такой буфер.

Отличительной особенностью применения VAO является то, что после настройки не надо подключать различные VBO, в которых хранятся атрибуты вершин, можно один раз подключить VAO и сразу приступать к выводу геометрии.

Для того, чтобы настроить VAO к работе и связать с ним все необходимые вершинные буферы, нужно произвести следующие шаги:

1. Создать массив с необходимыми данными (в качестве примера используется тот же треугольник, который был в начале лабораторной работы).

struct Vertex {

Vector2f Position;

Vector3f Color;

};

Vertex triangleMesh[] = {

{ Vector2f(-1.0f, -1.0f), Vector3f(1.0f, 0.0f, 0.0f) }, //1 вершина

{ Vector2f(0.0f, 1.0f), Vector3f(1.0f, 1.0f, 1.0f) }, //2 вершина

{ Vector2f(1.0f, -1.0f), Vector3f(0.0f, 0.0f, 1.0f)} //3 вершина

};

2. Создать VBO – glGenBuffers и VAO – glGenVertexArrays.

GLuint VAO = 0, VBO = 0;

glGenVertexArrays(1, &VAO); //Создать Vertex Array Object

glGenBuffers(1, &VBO); //Создать Vertex Buffer Object

3. Установить текущий массив вершин – glBindVertexArray

//Установить VAO текущим

glBindVertexArray(VAO);

4. Привязать вершинный буфер к текущему VAO – glBindBuffer

//Привязываем VBO к текущему VAO

glBindBuffer(GL_ARRAY_BUFFER, VBO);

5. Заполнить VBO данными – glBufferData

//Заполняем VBO данными треугольника

glBufferData(GL_ARRAY_BUFFER, sizeof(triangleMesh), triangleMesh, GL_STATIC_DRAW);

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

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

2. Передать по этому индексу данные.

В момент вызова функции glLinkProgram каждому атрибуту и юниформу назначается уникальный цифровой индекс, который используется для работы с ними из приложения. Для получения индекса по имени атрибута используются функция – glGetAttribLocation, а по имени юниформы – glGetUniformLocation.

Однако, у вершинных атрибутов есть одна важная особенность – можно использовать функцию glBindAttribLocation для назначения своих индексов атрибутам прежде, чем будет вызвана команда glLinkProgram.

Начиная с GLSL версии 3.30 появилась возможность сразу в коде вершинного шейдера строго задать положение для каждого атрибута. В частности, подобная возможность позволяет изначально привязать к желаемым индексам определенные виды атрибутов для целой группы шейдеров и, тем самым, получить унифицированный (более простой) способ задания атрибутов для всей группы сразу. Для этого была введена директива layout, используемая следующим образом:

layout (location = 3) in vec4 inNormal; //Задаем нормаль с индексом 3

layout (location = 0) in vec4 inPosition; //Задаем атрибут с индексом 0

layout (location = 1) in vec4 inColors[2]; //Задаем сразу два цвета с индексом 1 и 2

При этом число задает индекс (номер) для вершинного атрибута. Так, декларация для атрибута inColors резервирует сразу два положения – 1 и 2.

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

layout (location = 0) out vec4 firstColor;

layout (location = 1) out vec4 secondColor;

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

int maxVertexAttribsIndex = 0;

glGetIntegerv (GL_MAX_VERTEX_ATTRIBS, &maxVertexAttribsIndex);

Когда шейдерная программа собрана, индексы интересующих атрибутов получены и в VBO скопированы все необходимые данные, можно приступить к настройкам параметров вершинных атрибутов (применение макросов обусловлено удобством понимания кода):

#define VERTEX_ATRIB_COLOR 1

#define VERTEX_ATRIB_POSITION 0

#define BUFFER_OFFSET(i) ((GLbyte*)NULL + (i))

//Разрешить использование вершинного атрибута с индексом 0 (позиция)

glEnableVertexAttribArray(VERTEX_ATRIB_POSITION);

//Указываем параметры доступа вершинного атрибута к VBO

glVertexAttribPointer(VERTEX_ATRIB_POSITION, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), BUFFER_OFFSET(0));

//Разрешить использование вершинного атрибута с индексом 1 (цвет)

glEnableVertexAttribArray(VERTEX_ATRIB_COLOR);

//Указываем параметры доступа вершинного атрибута к VBO

glVertexAttribPointer(VERTEX_ATRIB_COLOR, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), BUFFER_OFFSET(sizeof(Vector2f)));

Команды рисования

После того, как произведена вся необходимая "предполетная" подготовка (подготовлена шейдерная программа, настроены VAO и произведено связывание с вершинными атрибутами), можно приступить к выводу геометрии. Простейшей такой командой вывода является:

void glDrawArrays(GLenum mode, GLint first, GLsizei count);

Первый параметр определяет примитив и задается константой примитива; второй параметр – начальный индекс массива; третий – количество элементов, которые необходимо вывести. Например:

glBindVertexArray(VAO);

glDrawArrays(GL_TRIANGLES, 0, 3);

Данный пример кода рисует примитив, состоящий из 3 вершин, заданных в массиве вершин. Вершины выводятся в соответствии с их порядком хранения в буфере памяти. Для вывода примитива с произвольным порядком обхода вершин, применяется следующая команда:

void glDrawElements(GLenum mode, GLsizei count, GLenum type, const GLvoid * indices);

Первый параметр определяет примитив и задается константой примитива; второй параметр – количество элементов в массиве; третий – тип массива; четвертый – указатель на массив индексов вершин (в каком порядке обходить вершины). Например:

glBindVertexArray(VAO);

GLuint idx[] = { 0u, 2u, 1u };

glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, idx);

Данный пример кода рисует тот же самый примитив, состоящий из 3 вершин, хранящимся в буфере памяти. Вершины выводятся в соответствии с их порядком (индексом вершины) указанном в массиве idx.

Включение и отключение режима сглаживания (антиэлайзинг).

Для установки режима сглаживания перед функцией отрисовки должна стоять команда glEnable() с соответствующей константой в качестве аргумента, а после отрисовки – команда glDisable() также с соответствующей константой. Выбор константы определяется примитивом, сглаживание которого должно быть включено (или отключено):

– GL_LINE_SMOOTH – сглаживание линий;

– GL_POLYGON_SMOOTH – сглаживание для полигонов.

Для сглаживания (антиэлайзинга) многоугольников используется константа GL_POLYGON_SMOOTH. Поскольку треугольники являются частными случаями многоугольника, то сглаживание для них осуществляется точно также, как для многоугольников.

Особенности режимов вывода примитивов.

Для изменения размера точки используется встроенная GLSL переменная gl_PointSize. Аргументом является натуральное число, определяющее размер точки в пикселях.

Аргументом команд glEnable() и glDisable() для включения / отключения режима программного изменения размера точки является константа GL_PROGRAM_POINT_SIZE.

Для изменения толщины линии используется команда:

void glLineWidth(GLfloat width);

Аргументом является натуральное вещественное число, определяющее толщину линии в пикселях.

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

Для выбора режима вывода многоугольников: только лицевые грани или обратные, – используется команда:

void glCullFace(GLenum mode)

Параметр определяет какой режим отрисовки будет установлен:

– GL_FRONT – лицевые;

– GL_BACK – обратные (по умолчанию);

Грань считается лицевой, если вершины перечисляются в направлении против часовой стрелки (в положительном направлении для левой системы координат), и обратной, если обход вершин производится по часовой стрелке (в отрицательном направлении).

Для задания режима обхода вершин: по часовой стрелке или против часовой, – используется команда:

void glFrontFace(GLenum mode);

Параметр определяет, как следует обходить вершины:

– GL_CW – по часовой стрелке;

– GL_CCW – против часовой стрелки (по умолчанию).

Аргументом команд glEnable() и glDisable() для включения/отключения режима учета лицевых и обратных граней является константа GL_CULL_FACE.

Для задания режима вывода многоугольников: в контурном виде (без заливки) или с заливкой, – используется команда:

void glPolygonMode(GLenum face, GLenum mode);

Первый параметр может быть только GL_FRONT_AND_BACK – лицевые и обратные полигоны. Второй параметр команды указывает способ изображения грани фигуры с помощью следующих констант:

– GL_FILL – вывод граней с заливкой;

– GL_LINE – каркасный вывод (только контуры);

– GL_POINT – выводятся только вершины.

Все описанные команды должны быть вызваны до функции отрисовки.