Сожалеем, но текст оригинала доступен только зарегистрированным пользователям. |
Глава 2. Наша первая программа на OpenGL.
Что нового вы узнаете в этой главе: - Как создать и скомпилировать шейдер. - Как рисовать при помощи OpenGL. - Как использовать фреймворк, поставляемый с книгой, для инициализации ваших программ и очистки памяти после ее использования.
В этой главе мы представляем простой фреймворк для создания программ, который будет в дальнейшем использоваться почти во всех примерах. Будет показано, как создавать главное окно с нашим фреймворком и как рисовать простую графику в этом окне. Так же вы увидите простые примеры использования: сам GLSL код, его компиляция и использование для отрисовки простых точек. Эта глава закончится вашим самым первым треугольником (на экране. п.п.).
Henerial
11.08.14 в 13:27
|
|
Создание простой программы Чтобы представить фреймворк, используемый в этой книге, мы начнем с простейшей программы. Этот фреймворк подключается к программе путем подключения (includе) sb6.h в исходный код. Это файл оглавления, который определяет пространство имен sb6(Супер Библия 6. п.п.), которое вмещает объявление класса программы, sb6::application, от которого мы можем вывести наши примеры. Во фреймворке так же присутствует множество дополнительных функций, а так же простая математическая библиотека vmath( вы можете вместо нее использовать GLM как более мощную. п.п. ), которая поможет вам в обработке чисел, присущей OpenGL. Для создания программы мы просто подключаем sb6.h, наследуем класс sb6::application, и (только в одном из наших файлов - main.cpp, к примеру) вызываем макрос DECLARE_MAIN с нашим переопределенным классом в качестве параметра. Это определяет главную точку входа в нашу программу и создает объект нашего класса и вызывает метод run(), который определяет главный цикл нашей программы (по аналогии с GlutMainLoop, п.п.). В свою очередь, это вызывает ф-ю инициализации - startup(), а так же ф-ю render(), теперь уже в цикле. В реализации по умолчанию обе эти ф-и является виртуальными и без реализации. Вы перегружаем ф-ю render() в нашем переопределенном классе и вписываем в нее наш код рисования. Сам фреймворк заботится о создании окна, обработке входных данных ( клавиатура/мышь и подобное, п.п. ), выводе отрисованных результатов в окно. Пример исходного кода дан в Исходнике 2.1, а вывод этой программы показан в Фигуре 2.1
Henerial
11.08.14 в 13:34
|
|
Фигура 2.1: Вывод нашей первой программы на OpenGL.
Henerial
12.08.14 в 12:22
|
|
Click here to view code image // Подключение заголовочного файла sb6.h #include "sb6.h"
// Наследуем my_application от sb6::application class my_application : public sb6::application { public: // Наша функция рендера void render(double currentTime) { // Простая очистка окна красным цветом static const GLfloat red[] = { 1.0f, 0.0f, 0.0f, 1.0f }; glClearBufferfv(GL_COLOR, 0, red); } };
// Наш единственный экземпляр DECLARE_MAIN DECLARE_MAIN(my_application);
Листинг 2.1: Наше первое OpenGL приложение
ximen
16.08.14 в 06:18
|
+3
|
Пример, показанный в Исходнике 2.1 просто очищает весь экран в красный (есть понятие цвет очистки - цвет, которым заливается кадр до отрисовки, п.п.). Тут представлена наша первая OpenGL функция - glClearBufferfv(). Прототипом glClearBufferfv() является:
Henerial
12.08.14 в 14:27
|
|
void glClearBufferfv(GLenum buffer, GLint drawBuffer, const GLfloat * value);
Henerial
12.08.14 в 14:27
|
|
Все OpenGL функции начинаются с gl заканчивается набором определенных суффиксов в конце имени функции. Это позволяет реализовать ограниченную форму перегрузки даже если язык программирования прямо этого не поддерживает. В нашел случае суффикс fv означает, что функция принимает вектор (v) флоатов (f), при этом массивы ( массивы в стиле С ) и векторы используются взаимозаменяемо.
Henerial
12.08.14 в 14:29
Названия всех функций OpenGL начинаются с "gl" и следуют определённым правилам построения, таким как указание типов параметров в конце имени. Это позволяет реализовать ограниченную форму перегрузки даже если язык программирования прямо этого не поддерживает. В таком случае, суффикс "fv" означает, что на вход функции подаётся вектор (v) значений с плавающей точкой (f). Векторы и массивы (как правило, представляющие собой указатели в языках, подобных C ) в OpenGL взаимозаменяемы.
Функция glClearBufferfv() говорит OpenGL очистить буфер, указываемый первым параметром (в данном случае GL_COLOR), значением, указанным в третьем параметре. Второй параметр, drawBuffer, используется в случае, если есть несколько буферов, которые могут быть очищены. Поскольку мы используем всего один буфер, а drawBuffer это индекс с нулевым основанием, в этом примере мы просто передаём 0. Цвет хранится в массиве red, состоящем из четырёх значений с плавающей точкой -- по одному на каждую компоненту: красный, зеленый, синий, альфа (и именно в таком порядке). Значение компонент "красный", "зелёный" и "синий" должно быть понятно из их названия. Альфа -- четвёртый компонент, связанный с цветом, и обычно используется для указания непрозрачности фрагмента. В таком случае, установка альфа в ноль делает фрагмент полностью прозрачным, а установка в единицу -- непрозрачным. Значение альфа сохраняется в выходном изображении и используется некоторыми частями OpenGL даже если вы этого не видите. Вы видите, что мы установили компоненты красный и альфа в единицу, а остальные в ноль. Это означает непрозрачный красный цвет. Результат работы этого приложения показан на Рисунке 2.1.
Это начальное приложение не особо интересно1, т.к. всё, что оно делает, это заполнение окна сплошным красным цветом. Заметьте, что наша функция render() принимает единственный параметр -- currentTime. Он содержит время, прошедшее с момента запуска приложения, и мы можем использовать его для создания простой анимации. Например, мы можем использовать его для изменения цвета, которым мы закрашиваем окно. Изменённая функция render()2 представлена в Листинге 2.2.
1. Этот пример особенно не интересен, если вы читаете черно-белый вариант этой книги. 2. Если вы решите скопировать этот код в ваш собственный пример, вам потребуется подключить <math.h> для объявления функций sin() и cos().
ximen
16.08.14 в 06:59
|
|
// Наша функция рендера 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); }
Листинг 2.2: Анимация цвета с течением времени
ximen
16.08.14 в 07:01
|
|
Теперь наше окно изменяет цвет с красного через жёлтый, оранжевый, зелёный и чёрный снова на красный. Всё ещё не особо захватывающе, но хотя бы уже что-то делает.
ximen
16.08.14 в 07:02
|
|
Использование шейдеров Как мы уже упоминали в Главе 1, OpenGL работает путём соединения в единое целое нескольких мини-программ, называемых шейдеры. Когда вы рисуете, графический процессор исполняет ваши шейдеры и соединяет их входы и выходы в конвейер пока в конце не получатся пиксели3. Что нарисовать хоть что-то, вам потребуется как минимум пара шейдеров. ---------------------------- 3. Вообще-то, существуют случаи, когда OpenGL вообще не создаёт пиксели. Об этом мы поговорим позже, а сейчас давайте просто нарисуем несколько картинок. ---------------------------- Шейдеры OpenGL пишутся на языке OpenGL Shading Language или GLSL. Этот язык происходит от C, но со временем был изменён, чтобы лучше выполняться на графических процессорах. Если вы знакомы с C, то разобраться с GLSL должно быть не сложно. Компилятор для этого языка встроен в OpenGL. Исходный код вашего шейдера помещается в шейдерный объект и компилируется, а затем множество шейдерных объектов могут быть связаны вместе для создания программного объекта. Каждый программный объект может содержать шейдеры различных стадий. В OpenGL существуют следующие стадии шейдеров: вершинные шейдеры, шейдеры управления тесселяцией, геометрические шейдеры, фрагментные шейдеры и вычислительные шейдеры. Минимальная полезная конфигурация конвейера содержит только вершинный4 (или только вычислительный) шейдер, но если вы хотите увидеть какие-нибудь пикселы на экране, вам также потребуется фрагментный шейдер. ---------------------------- 4. Если вы попытаетесь нарисовать что-нибудь, когда в вашем конвейере нет вершинного шейдера, результат получится непредсказуемый и почти обязательно не тот, на который вы рассчитывали. ---------------------------- Наша первая пара шейдеров крайне проста. Листинг 2.3 показывает наш первый вершинный шейдер. Первая строка содержит объявление #version 430 core, которое говорит компилятору шейдера, что мы намерены использовать шейдерный язык версии 4.3. Заметьте, что мы использовали ключевое слово core для указания того, что мы будем пользоваться возможностями только базового профиля (core profile) OpenGL.
Далее идёт объявление функции main, с которой начинает выполняться шейдер, в точности, как и обычная программа на языке C. Разница заключается в том, что функция main в GLSL не имеет параметров. Внутри функции main мы присваиваем значение переменной gl_Position, которая является частью того, что связывает шейдер с остальным OpenGL. Все переменные, имя которых начинается с "gl_", являются частью OpenGL и соединяют шейдеры друг с другом или с частями фиксированной функциональности OpenGL. В вершинном шейдере gl_Position представляет выходное положение вершины. Значение, которое мы присваиваем (vec4(0.0, 0.0, 0.5, 1.0)), располагает вершину точно в середине однородной системы координат (clip-space), которая требуется для следующей стадии конвейера OpenGL.
ximen
16.08.14 в 08:05
|
|
#version 430 core void main(void) { gl_Position = vec4(0.0, 0.0, 0.5, 1.0); }
Листинг 2.3: Наш первый вершинный шейдер
ximen
16.08.14 в 08:05
|
|
Наш вершинный шейдер представлен на Листинге 2.4. И снова, он крайне прост. Он также начинается с объявления #version 430 core. Далее color объявляется выходной переменной с помощью ключевого слова out. Значение выходных переменных фрагментного шейдера будет отправлено на экран или в окно. В функции main этой переменной присваивается постоянное значение. По умолчанию, это значение сразу отображается на экране и представляет собой вектор из четырёх значений с плавающей точкой, по одному на красную, зелёную, синюю и альфа компоненты, в точности как параметр glClearBufferfv(). В этом шейдере мы использовали значение vec4(0.0, 0.8, 1.0, 1.0), что соответствует сине-зелёному цвету.
ximen
17.08.14 в 06:53
|
|
#version 430 core out vec4 color; void main(void) { color = vec4(0.0, 0.8, 1.0, 1.0); }
Листинг 2.4: Наш первый фрагментный шейдер
ximen
17.08.14 в 06:54
|
|
Теперь, когда у нас есть и вершинный и фрагментный шейдеры, пришло время их скомпилировать и связать в программу, которая может быть выполнена OpenGL. Это аналогично тому, как программы, написанные на C++ или подобном языке компилируются и связываются для получения исполняемого файла. Код для связи наших шейдеров в программный объект показан в Листинге 2.5.
ximen
17.08.14 в 06:57
|
|
GLuint compile_shaders(void) { GLuint vertex_shader; GLuint fragment_shader; GLuint program;
// Исходный код вершинного шейдера static const GLchar * vertex_shader_source[] = { "#version 430 core \n" " \n" "void main(void) \n" "{ \n" " gl_Position = vec4(0.0, 0.0, 0.5, 1.0); \n" "} \n" }; // Исходный код фрагментного шейдера static const GLchar * fragment_shader_source[] = { "#version 430 core \n" " \n" "out vec4 color; \n" " \n" "void main(void) \n" "{ \n" " color = vec4(0.0, 0.8, 1.0, 1.0); \n" "} \n" };
// Создание и компиляция вершинного шейдера vertex_shader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertex_shader, 1, vertex_shader_source, NULL); glCompileShader(vertex_shader);
// Создание и компиляция фрагментого шейдера fragment_shader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragment_shader, 1, fragment_shader_source, NULL); glCompileShader(fragment_shader);
// Создание программы, прикрепление шейдеров и их связываение. program = glCreateProgram(); glAttachShader(program, vertex_shader); glAttachShader(program, fragment_shader); glLinkProgram(program);
// Удаляем шейдеры т.к. они есть в программе glDeleteShader(vertex_shader); glDeleteShader(fragment_shader); return program; }
Листинг 2.5: Компиляция простого шейдера
ximen
17.08.14 в 07:00
|
|
В листинге 2.5, мы использовали несколько новых функций: • glCreateShader() создает пустой объект шейдера, готовый принять исходный код и скомпилироваться. • glShaderSource() передает исходный код шейдера в шейдерный объект таким образом, чтобы он хранил копию этого кода. • glCompileShader() компилирует любой код, который хранит шейдер. • glCreateProgram() создает объект программы, к которому вы можете присоединить объекты шейдеров. • glAttachShader() присодиняет объект шейдера к объекту программы. • glLinkProgram() собирает все объекты шейдеров, присоединенные к объекту программы, вместе. • glDeleteShader() удаляет объект шейдера.
Как только шейдер был собран в объект программы, программа содержит байт-код, и шейдер больше не нужен. Исходный код шейдеров из листинга 2.3 и 2.4 включены в нашу программу как константные строки, которые передаются функции glShaderSource(), которая копирует их в объект шейдера, создаваемый с помощью функции glCreateShader(). Объект шейдера хранит копию нашего исходного кода, и потом, когда мы вызываем функцию glCompileShader(), она компилирует GLSL-код шейдера в промежуточное двоичное представление (intermediate binary representation), которое также сохраняется в объекте шейдера. Объект программы представляет собой собранный исполняемый объект???, который мы будем использовать для рендеринга. Мы присоединяем наши шейдеры к объекту программы, используя функцию glAttachShader() и потом вызываем glLinkProgram(), который собирает объекты вместе в код, который может быть исполнен на графическом процессоре. Присоединение объекта шейдера к объекту программы создает ссылку на шейдер и поэтому мы можем удалить ее, зная, что объект программы будет удерживать содержимое шейдера до тех пор, пока оно ему нужен. Функция compile_shaders в листинге 2.5 возвращает только что созданный объект программы.
Когда мы вызываем эту функцию, мы должны где-то сохранить возвращаемый объект программы, чтобы использовать его для отрисовки. Мы также не хотим компилировать заново целую программу каждый раз, когда хотим ее использовать. Так что нам нужна функция, вызываемая лишь однажды, когда программа только запускается. Библиотека sb6 дает возможность использовать такую функцию: application::startup(), которую мы можем перегрузить в нашем примере и выполнить любую единовременную инициализацию.
Последнее, что нам надо сделать до того, как мы сможем нарисовать что-либо - это создать объект массива вершин (vertex array object, VAO), представляющим этап выборки вершин конвейера OpenGL и используемым для передачи входных данных в вершинный шейдер. Так как сейчас наш вершинный шейдер не получает никаких входных данных, нам ничего не нужно делать с объектом массива вершин. Тем не менее, нам все-таки надо создать его, чтобы OpenGL позволил нам рисовать. Для этого мы вызываем функцию glGenVertexArrays(), и, чтобы присоединить его к нашему контексту, вызываем функцию glBindVertexArray(). Их прототипы выглядят так
vk955
17.08.14 в 09:15
|
|
void glGenVertexArrays(GLsizei n, GLuint * arrays);
void glBindVertexArray(GLuint array);
vk955
12.08.14 в 18:14
|
|
Объект массива вершин поддерживает все состояния, связанные с входными данными конвейера OpenGL. Мы добавим вызовы функций glGenVertexArrays() и glBindVertexArray() в нашу функцию startup().
В листинге 2.6 представлен перегруженный метод startup() класса sb6::application, в который мы помещаем наш код инициализации. Снова, как и в случае с render(), функция startup() определена как (пустая / нереализованная) виртуальная функция sb6::application, и вызывается автоматически функцией run(). Из функции startup(), мы вызываем compile_shaders и сохраняем возвращаемый объект программы в переменной-члене нашего класса. Когда наше приложение завершает работу, нам следует освободить ресурсы, и поэтому у нас также есть перегруженная функция shutdown(), в которой мы удаляем объект программы, созданный при запуске. Также, как для завершения работы с шейдерами мы вызывали функцию glDeleteShader(), для объекта программы мы вызываем glDeleteProgram(). В нашей функции shutdown(), мы также удаляем объект массива вершин, созданный в функции startup().
vk955
17.08.14 в 09:24
|
|
class my_application : public sb6::application { public: // <обрезано>
void startup() { rendering_program = compile_shaders(); glGenVertexArrays(1, &vertex_array_object); glBindVertexArray(vertex_array_object); }
void shutdown() { glDeleteVertexArrays(1, &vertex_array_object); glDeleteProgram(rendering_program); glDeleteVertexArrays(1, &vertex_array_object); } private: GLuint rendering_program; GLuint vertex_array_object; };
Listing 2.6: Создание переменной программы, являющейся членом класса.
vk955
12.08.14 в 18:16
|
|
Теперь, когда у нас есть объект программы, мы должны выполнить шейдеры, содержащиеся в нем, и нарисовать что-нибудь на экране. Мы изменим нашу функцию render(), чтобы вызвать glUseProgram() и указать OpenGL использовать наш объект программы для рендеринга и потом вызвать нашу первую команду отрисовки glDrawArrays(). Обновленная версия показа в листинге 2.7.
vk955
17.08.14 в 09:38
|
|
// Наша функция отрисовки 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);
// Рисуем одну точку glDrawArrays(GL_POINTS, 0, 1); }
Listing 2.7: Рендеринг точки
vk955
12.08.14 в 18:36
|
|
Функция glDrawArrays посылает вершины в конвейер OpenGL. Ее прототип выглядит так
vk955
17.08.14 в 09:46
|
|
void glDrawArrays(GLenum mode, GLint first, GLsizei count);
vk955
12.08.14 в 18:34
|
|
Для каждой вершины вызывается вершинный шейдер (тот, что в листинге 2.3). Первый параметр glDrawArrays() - переключатель режима, который сообщает OpenGL, какой тип графического примитива мы хотим нарисовать. В данном случае мы указали GL_POINTS, так как хотим нарисовать единичную точку. Второй параметр (first) не актуален для данного примера, поэтому мы установим его в ноль. Наконец, последний параметр - число вершин для отрисовки. Каждая точка представлена одной вершиной, поэтому мы указываем OpenGL отобразить только одну вершину, в результате только одна точка и отображается. Результат выполнения программы показан на рисунке 2.2.
vk955
17.08.14 в 09:48
|
|
Рисунок 2.2: Рендеринг нашей первой точки
vk955
17.08.14 в 09:49
|
|
Как вы можете видеть, появилась крошечная точка в середине окна. Для вашего удобства, мы увеличили точку и показали ее на вставке в правом нижнем углу изображения. Поздравляем! Вы завершили ваш самый первый рендеринг в OpenGL. И хотя все это было пока еще не особо впечатляющим, это основа для более интересного рендеринга и доказывает, что наше фреймворк приложение и первые супер простые шейдеры работают.
Чтобы сделать нашу точку чуть более видимой, мы можем указать OpenGL нарисовать ее крупнее, чем в один пиксель. Чтобы сделать это, мы вызовем функцию glPointSize(), чей прототип выглядит так:
void glPointSize(GLfloat size);
Эта функция устанавливает диаметр точки в пикселях через параметр size. Максимальное значение, которое вы можете указать для точек определяется реализацией, но OpenGL гарантирует, что оно будет не меньше 64 пикселей. Добавив следующую линию
glPointSize(40.0f);
в нашу функцию рендеринга в листинге 2.7, мы устанавливаем диаметр точек равный 40 пикселям, что показано в рисунке 2.3.
vk955
17.08.14 в 09:53
|
|
Рисунок 2.3: Создание нашей первой большой точки
vk955
17.08.14 в 09:56
|
|
Рисование нашего первого треугольника Рисование одной единственной точки не так захватывающе (даже если она действительно большая!) - мы уже упомянули, что OpenGL поддерживает много разных типов примитивов, из которых самыми важными являются точки, линии и треугольники. В нашем игрушечном примере, мы рисуем точку, передавая флаг GL_POINTS в функцию glDrawArrays(). Что мы на самом деле хотим сделать, так это рисовать линии и треугольники. Как вы наверное уже догадались, мы могли бы передать значения GL_LINES или GL_TRIANGLES в glDrawArrays(), но но есть одна загвоздка: вершинный шейдер из листинга 2.3 помещает каждую вершину в одно и тоже место, прямо в середину пространства отсечения. Для точек это нормально, когда OpenGL за вас присваивает точкам область, но для линий и треугольников, имеющим две и более вершины, в том же месте производит вырожденный примитив: линия с нулевой длиной или треугольник с нулевой площадью. Если мы попытаемся нарисовать что-либо помимо точек с помощью этого шейдера, мы вообще не получим никаких выходных данных, так как, все примитивы будут вырожденными. Чтобы это исправить, нам надо изменить наш вершинный шейдер, чтобы присвоить другие координаты для каждой вершины.
К счастью, GLSL дает возможность использовать специальный параметр входных данных вершинного шейдера - gl_VertexID, который является индексом вершины, которая в данный момент обрабатывается. Входной параметр gl_VertexID начинает отсчитываться от значения, передаваемого в первом параметре функции glDrawArrays, и увеличивается на единицу при каждой следующей вершине (третий параметр функции glDrawArrays). Этот входной параметр один из многих встроенных переменных, предоставляемых языком GLSL, которые представляют данные, генерируемые OpenGL или те, что вам следует генерировать самим в шейдере и передавать в OpenGL (gl_Position, которую мы недавно рассмотрели, является другим примером встроенной переменной). Мы можем использовать этот индекс, чтобы присвоить другие позиции каждой вершине (см. листинг 2.8, который делает именно это).
vk955
17.08.14 в 10:00
|
|
#version 430 core
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));
// Индексируем элемент в нашем массиве, используя gl_VertexID gl_Position = vertices[gl_VertexID]; }
Листинг 2.8: Порождение множества вершин в вершинном шейдере
vk955
12.08.14 в 18:41
|
|
Используя шейдер из листинга 2.8, мы можем присвоить другие позиции вершинам, основываясь на их значении параметра gl_VertexID. Точки в вершинах массива образуют треугольник, и если мы модифицируем нашу функцию рендеринга для передачи флага GL_TRIANGLES в glDrawArrays вместо GL_POINTS, как показано в листинге 2.9, то получим картинку, как на рисунке 2.4.
Рисунок 2.4: Наш самый первый OpenGL-треугольник
vk955
17.08.14 в 10:41
|
|
Click here to view code image
// Наша функция рендеринга void render(double currentTime) { const GLfloat color[] = { 0.0f, 0.2f, 0.0f, 1.0f }; glClearBufferfv(GL_COLOR, 0, color);
// Используем объект программы, который мы создали ранее, для рендеринга glUseProgram(rendering_program);
// Рисуем один треугольник glDrawArrays(GL_TRIANGLES, 0, 3); }
Листинг 2.9: Рендеринг одного треугольника
vk955
12.08.14 в 18:44
|
|
Заключение На этом мы завершаем создание нашей первой программы OpenGL. Если кратко, мы рассмотрели, как передавать данные из вашего приложения в шейдер, как передавать ваши собственные входные данные в вершинный шейдер, а также как передавать данные из одного этапа обработки шейдеров в другой и так далее.
В этой главе, мы поверхностно изучили библиотеку sb6, скомпилировали шейдер, очистили окно и нарисовали точки с треугольниками. Вы увидели, как изменить размер точек, используя функцию glPointSize и применили в первый раз команду рисования - glDrawArrays.
vk955
17.08.14 в 10:44
|
|