Это пока черновой вариант. Материал будет дорабатываться в течение семестра.
OpenGL - графический программный интерфейс, который позволяет создавать интерактивные графические 3D-приложения. 3D-графика требует больших объемов вычислений, и современные видеокарты оснащены графическими процессорами для ускорения расчетов в 3D-графике. OpenGL и DirectX - два основных программных интерфейса, которые позволяют получить доступ к вычислительным возможностям видеокарты.
OpenGL - кроссплатформенный интерфейс. Он работает под Windows, Linux, OS X и другими операционными системами. Версия OpenGL ES предназначена для работы на мобильных устройствах (Android, iOS). WebGL - вариант OpenGL для запуска в браузере.
OpenGL - это только интерфейс (набор сигнатур функций) и не имеет официальной реализации. Производители железа и операционных систем должны поставлять собственные реализации OpenGL, что и обеспечивает кроссплатформенность.
OpenGL, как стандарт, был предложен в 1992м году компанией Silicon Graphics Inc. За основу был взят их графический интерфейс Iris GL для суперкомпьютеров IRIS. Развитие стандарта было поручено консорциуму ARB (Architecture Review Board), председателем которого стала Silicon Graphics Inc.
В конце 1990х годов появились первые видеокарты с графическими процессорами, что позволило значительно ускорить выполнение графических программ.
Конкуренция между производителями видеокарт привела к быстрому их развитию и добавлению новых возможностей. Но противоречия внутри консорциума ARB задерживали стандартизацию этих возможностей. Лишь в 2004м с трудом вышла версия OpenGL 2.0, в которую был включен язык шейдеров GLSL.
В 2006м году консорциум ARB передал развитие OpenGL консорциуму Khronos Group. Вскоре после этого, в 2008м вышла новая версия OpenGL 3.0, в которой были стандартизированы новые возможности, а множество старых функций были объявлены устаревшими. Это разделило историю OpenGL на 2 периода: старый и современный OpenGL (Modern OpenGL).
Тем не менее, драйвера видеокарт продолжают поддерживать устаревшие функции OpenGL для обратной совместимости. Поддержка старых функций приводит к большим накладным расходам в драйверах видеокарт, что негативно влияет на скорость работы графических приложений.
В 2016м году Khronos Group предложил совершенно новый графический интерфейс Vulkan, который должен стать заменой OpenGL, лишенный груза обратной совместимости и обеспечивающий меньшие накладные расходы в драйверах.
Однако Vulkan значительно более низкоуровневый, чем OpenGL, и не может являться прямой заменой. Вполне возможно, что OpenGL продолжит существовать, возможно в виде надстройки над Вулканом. OpenGL остается хорошим средством изучения азов трехмерной графики.
Каркас графического приложения в упрощенном виде выглядит следующим образом:
void main()
{
Инициализация графического контекста
Инициализация функций OpenGL
Создание буферов, текстур, шейдеров и т.д.
while (окно не закрыто)
{
Обработка событий ввода (клавиатура, мышь)
Обновление 3D-сцены
Рендеринг кадра
Переключение буферов
}
Удаление буферов, текстур, шейдеров и т.д.
}
Термины:
Для работы графическому приложению необходимо использовать ресурсы видеокарты и операционной системы. При этом ресурсы должны быть изолированы от других приложений. А результат рендеринга должен быть показан в окне операционной системы. Для этого и используется вспомогательный объект - графический контекст. При создании контекста происходит создание окна и буферов для рендеринга.
Создание графического контекста отличается в разных операционных системах. Поэтому для удобства мы будем использовать библиотеку GLFW, которая обеспечивает простой и единообразный способ создания контекста.
//Создаем окно (графический контекст)
GLFWwindow* window = glfwCreateWindow(800, 600, "MIPT OpenGL demos", NULL, NULL);
//Делаем этот контекст текущим
glfwMakeContextCurrent(window);
Перед началом рендеринга необходимо выбрать текущий контекст, куда и будет происходить вывод. В приложении может быть несколько контекстов. В один момент времени в один контекст может рендерить только один поток.
Поскольку OpenGL сам по себе это лишь интерфейс, а каждая платформа имеет свою реализацию, то статическая линковка к функциям OpenGL не возможна. Поэтому необходимо на этапе выполнения динамически инициализировать указатели на функции OpenGL. Для этого мы будем использовать библиотеку GLEW:
glewExperimental = GL_TRUE;
glewInit();
Для обработки событий ввода необходимо зарегистрировать в GLFW несколько функций обратного вызова:
glfwSetKeyCallback(window, keyCallback);
Функции обратного вызова вызываются при проверке новых событий каждую итерацию цикла:
while (!glfwWindowShouldClose(window))
{
glfwPollEvents();
Графическое приложение обычно имеет 2 буфера: передний (front) и задний (back). Содержимое переднего буфера выводится на экран монитора, а рендеринг осуществляется в невидимый задний буфер. После завершения рендеринга кадра необходимо поменять буферы местами:
glfwSwapBuffers(window);
}
Далее рассмотрим загрузку 3D-моделей и рендеринг.
Термин:
В 3D-графике используются полигональные 3D-модели, которые являются наиболее простым и универсальным способом представления 3D-объектов. Полигональная 3D-модель - это поверхность, состоящая из полигонов. В современном OpenGL полигон - это треугольник.
Треугольник состоит из 3х вершин. Каждая вершина имеет набор атрибутов. Широко используемые атрибуты:
OpenGL позволяет универсально работать с почти любыми атрибутами. Можно задавать вершинам, например, веса, напряжения, силы и любые другие параметры по желанию.
Важно: 3D-модель описывается через атрибуты вершин. Нельзя задавать атрибуты ребрам или полигонам.
Каждый атрибут может иметь 1, 2, 3 или 4 компоненты. Например, координаты имеют 3 компоненты: x, y, z. Текстурные координаты обычно имеют 2 компоненты: s, t. Цвет имеет 3 компоненты: r, g, b, но часто дополняется 4й компонентой a (alpha - прозрачность).
Все компоненты одного атрибута должны быть одного типа. Типами могут быть: GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_INT, GL_UNSIGNED_INT, GL_HALF_FLOAT, GL_FLOAT, GL_DOUBLE, GL_FIXED и другие.
Важно: в рамках одной 3D-модели количество атрибутов, количество компонент каждого атрибута и их типы должны быть одинаковыми для всех вершин.
Значения атрибутов вершин хранятся в буферах в видеопамяти. Можно выделить 3 варианта хранения атрибутов.
Но как видеокарта узнает, какие вершины образуют треугольники? Есть 2 варианта:
Чтобы создать буфер, используется команда glGenBuffers. Она создает на видеокарте 1 или несколько объектов buffer object и возвращает их целочисленные идентификаторы.
GLuint vbo;
glGenBuffers(1, &vbo); //В переменную vbo будет записан идентификатор буфера
OpenGL требует, чтобы перед использованием буфер был «привязан», т.е. сделан активным буфером.
glBindBuffer(GL_ARRAY_BUFFER, vbo);
Команда glBufferData выделяет память на видеокарте и копирует туда содержимое массива из оперативной памяти. После этого массив в оперативной памяти можно удалить.
float points[] =
{
0.0f, 0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
-0.5f, -0.5f, 0.0f
};
//Цель буфера, размер, указатель, предполагаемый способ использования буфера
glBufferData(GL_ARRAY_BUFFER, 9 * sizeof(float), points, GL_STATIC_DRAW);
Эту процедуру нужно повторить для всех буферов 3D-модели, если разные атрибуты хранятся в разных буферах.
Всё аналогично для индексного буфера, только цель буфера будет GL_ELEMENT_ARRAY_BUFFER.
unsigned short indices[] =
{
0, 1, 2,
0, 2, 3,
4, 0, 3,
4, 3, 5,
5, 6, 2,
5, 2, 3
};
//Создаем ещё один буфер для хранения индексов
GLuint ibo;
glGenBuffers(1, &ibo);
//Делаем этот буфер текущим
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
//Копируем содержимое массива индексов в буфер на видеокарте
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
Но как видеокарта узнает, какие буферы к какому мешу относятся, в каком буфере какой вершинный атрибут лежит, сколько у него компонентов и тип данных? Это необходимо явно указать.
Для хранения настроек меша используется специальный объект vertex array object:
//Создаем объект vertex array object
GLuint vao;
glGenVertexArrays(1, &vao);
//Активируем объект
glBindVertexArray(vao);
//Активируем буфер с вершинными атрибутами
glBindBuffer(GL_ARRAY_BUFFER, vbo);
//Включаем 0й вершинный атрибут - координаты
glEnableVertexAttribArray(0);
//Включаем 1й вершинный атрибут - цвета
glEnableVertexAttribArray(1);
//Устанавливаем настройки: 0й атрибут, 3 компоненты типа GL_FLOAT, не нужно нормализовать, 0 - значения расположены в массиве впритык, 0 - сдвиг от начала
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
//Устанавливаем настройки: 1й атрибут, 4 компоненты типа GL_FLOAT, не нужно нормализовать, 0 - значения расположены в массиве впритык, 84 - сдвиг от начала массива
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, (void*)84);
//Подключаем буфер с индексами
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
//Отключаем vertex array object
glBindVertexArray(0);
В этом примере используется один буфер, в котором хранятся 2 вершинных атрибута: сначала все координаты, затем все цвета.
В команде glVertexAttribPointer указывается:
Замечание:
Нужно осторожно относиться к дублированию вершин при работе с индексными мешами. 2 вершины считаются идентичными, если у них совпадают ВСЕ вершинные атрибуты. Обычно это справедливо для гладких мешей.
Но возьмем, например, куб. На первый взгляд, он содержит 8 вершин. Однако, каждая из этих вершин принадлежит 3м граням с разными нормалями. Поэтому на самом деле 3D-модель куба будет содержать 24 неповторяющихся вершины (4 вершины на каждую из 6ти граней).
Для запуска отрисовки меша нужно подключить соответствующий ему vertex array object и вызвать команду отрисовки. Семейство команд glDrawArrays предназначено для безиндексных мешей, семейство glDrawElements - для индексных мешей.
Вариант без индексов:
//Подключаем vertex array object с настойками полигональной модели
glBindVertexArray(_vao);
//Геометрический примитив, сдвиг от начала буфера с вершинными атрибутами, количество вершин
glDrawArrays(GL_TRIANGLES, 0, 36);
Вариант с индексами:
//Подключаем vertex array object с настойками полигональной модели
glBindVertexArray(_vao);
//Геометрический примитив, количество индексов в буфере с индексами, тип данных в буфере с индексами, сдвиг от начала буфера с индексами
glDrawElements(GL_TRIANGLES, 24, GL_UNSIGNED_SHORT, 0);
Рендеринг 3D-сцены - довольно сложный процесс, который разбивается на ряд небольших шагов. Последовательность этих шагов называют графическим конвейером. Шаги графического конвейера начинают выполняться на видеокарте после запуска конвейера специальной командой.
В упрощенном виде конвейер представлен на рисунке:
Некоторые шаги конвейера жестко зашиты в видеокарте. На их выполнение можно влиять лишь косвенно, изменяя отдельные параметры.
Другие шаги конвейера можно варьировать более гибко - путем написания шейдеров - программ на специальном языке программирования. Шейдеры выполняются на графическом процессоре. Шейдер должен принять данные с предыдущего шага конвейера, преобразовать их и передать на следующий шаг конвейера.
Всего имеется 5 шагов конвейера, где можно использовать шейдеры. Из них только 2 являются обязательными: вершинный и фрагментный.
В основной программе рендеринг одной 3D-модели выглядит следующим образом:
Изменение состояния OpenGL (установка параметров, необходимых для данной 3D-модели)
Запуск графического конвейера (glDrawArrays, glDrawElements и другие)
Графический конвейер начинает свою работу с вызовов glDrawArrays, glDrawElements и их вариантов.
Изначально координаты вершин меша заданы в произвольной локальной системе координат. Чтобы отобразить 3D-модель на экране монитора, необходимо преобразовать все вершины модели в экранную систему координат (ось X направлена вправо, ось Y - вверх, координаты измеряются в пикселях).
Это преобразование зависит от нескольких факторов: расположения 3D-модели относительно других 3D-моделей в сцене, расположения и ориентации виртуальной камеры, угла обзора, размеров экрана и т.д. Преобразование разбивается на несколько подэтапов, каждый из которых можно представить в виде матрицы, на которую умножается вектор с координатами.
На входе: 3 локальные координаты вершины. На выходе: экранные координаты XY в пикселях экрана и координата Z - глубина. Глубина пропорциональна расстоянию от экрана до вершины.
Преобразование вершин разбивается на 2 части: 1я часть выполняется в вершинном шейдере и полностью определяется нами, 2я часть жестко зашита в видеокарте. После завершения вершинного шейдера видеокарта проверяет, попадает ли вершина в объем видимости: будет ли она видна на экране. Если нет - вершина отбрасывается.
Важное следствие: вершинный шейдер выполняется для всех вершин меша.
Помимо преобразования координат вершин вершинный шейдер может также обрабатывать другие вершинные атрибуты и передавать их на выход.
На этом этапе видеокарта разбивает треугольники на фрагменты. Это возможно, т.к. координаты вершин треугольников уже находятся в экранной системе координат. Разбиваются только те треугольники, которые попадают на экран, невидимые вершины были отброшены на предыдущем этапе.
Каждый фрагмент соответствует одному пикселю экрана.
На этом же этапе происходит интерполяция выходных значений вершинного шейдера. Каждому фрагменту сопоставляются значения, интерполированные с учетом расстояния до вершин треугольника.
Также каждому фрагменту сопоставляется интерполированная глубина, рассчитанная на основе глубин вершин треугольника.
Для каждого фрагмента вызывается фрагментный шейдер, на вход которому передаются интерполированные значения из вершинного шейдера.
Задача фрагментного шейдера - рассчитать цвет фрагмента. Также фрагментный шейдер может отбросить (уничтожить) фрагмент или изменить его глубину.
Для каждого пикселя экрана хранится текущий цвет в буфере цвета и текущая глубина в буфере глубины.
После завершения фрагментного шейдера фрагмент должен пройти ряд тестов, самым важным из которых является тест глубины. Если глубина фрагмента больше глубины пикселя, то он уничтожается. Если глубина меньше глубины пикселя, то возможны 2 варианта в зависимости от настроек:
В обоих случаях глубина пикселя будет заменена на глубину фрагмента.
OpenGL позволяет отключать тест глубины, изменять функцию сравнения глубин, отключать запись в буфер глубины и даже запись в буфер цвета, задавать функцию смешивания цветов.