Расширения
Из первого урока вы узнали как создать контекст с поддержкой OpenGL 3.3, однако одного контекста мало, необходимо еще получить доступ к функционалу OpenGL 3.3 Для этого предусмотрен специальный механизм получения указателей на функции из драйвера видеокарты, который был мельком упомянут в предыдущем уроке, для Windows это функция wglGetProcAddress.
Стоит также оговориться почему нам необходима функция wglGetProcAddress, дело в том что при сборке приложения в Windows нам доступна только библиотека с поддержкой функционала OpenGL 1.1, а все функции из старших версий мы должны загружать уже в процессе работы приложения, динамически. Не во всех ОС это так, например в Linux производители драйверов для видеокарт распространяют библиотеку с поддержкой именно того функционал, который есть в драйвере, его можно подключать уже в момент сборки приложения, статически. Но даже там есть возможность загружать расширения в процессе работы приложения, сделано это для того, чтобы бинарные файлы собранные на одном компьютере могли быть запущены на другом, возможно с совершенно другим драйвером.
Кстати, называть "расширениями" функции находящиеся в ядре OpenGL 3.3 не совсем верно, это уже не расширение функционала, это стандартный набор для версии 3.3, однако по старой традиции обычно все функции которые надо грузить динамически называют "расширениями".
Динамическая загрузка функция достаточно проста, вы уже видели как используется функция wglGetProcAddress в прошлом уроке - надо создать контекст OpenGL нужной версии и можно приступать к загрузке функций. Для динамической загрузки функций нам понадобится файл с прототипами этих функций glext.h. Этот свободно доступен с сайта opengl.org и с выходом новых версий стандарта OpenGL он обновляется. В архиве с исходниками к этому уроку этот файл уже есть.
Используя прототипы мы определим указатели на функции и после создания контекста с поддержкой OpenGL 3.3 загрузим их динамически. Выглядит это так:
PFNGLCREATEPROGRAMPROC glCreateProgram = NULL;
glCreateProgram = (PFNGLCREATEPROGRAMPROC)wglGetProcAddress("glCreateProgram");
if (glCreateProgram == NULL)
{
LOG_ERROR("Loading extension 'glCreateProgram' fail (%d)\n", GetLastError());
return false;
}
В исходниках приложенных к этому уроку процесс получения указателя и проверки на успех немного автоматизирован с использованием макроса OPENGL_GET_PROC:
#define OPENGL_GET_PROC(p,n) \
n = (p)wglGetProcAddress(#n); \
if (NULL == n) \
{ \
LOG_ERROR("Loading extension '%s' fail (%d)\n", #n, GetLastError()); \
return false; \
}
OPENGL_GET_PROC(PFNGLCREATEPROGRAMPROC, glCreateProgram);
Также существуют различные готовые библиотеки, которые позволяют еще больше автоматизировать работу с расширениями, например библиотека GLEW. Однако в наших уроках нм не понадобиться большой список расширений, поэтому будем использовать "ручной" способ загрузки, представленный выше.
Шейдеры
Напоминаю, для тех кто забыл, что фиксированный конвейер обработки данных видеокартой был убран из OpenGL версии 3.0 и выше. Теперь такие данные как вершины и пиксели обрабатываются специальном пользовательской программой - шейдером, соответственно вершинным и фрагментным (пиксельным), которые вкупе представляют собой шейдерную программу.
Без использования хотя бы простейшего шейдера мы не получим никакой картинки на экране своего монитора, таким образом, вторая ступень для вывода треугольника на экран - загрузка и подготовка к работе шейдерной программы.
Для начала определимся с функционалом, который нам нужен от шейдерной программы. Необходимо вывести на экран треугольник с разноцветными вершинами, таким образом на каждую вершину нам понадобится два параметра - позиция и цвет, такие параметры в шейдере называются атрибутами (attributes). Атрибуты предназначены для каждой вершины индивидуально. Помимо атрибутов в шейдере также используются юниформы (uniforms), они общие для всех, обрабатываемых шейдером, вершин, либо для определенной группы вершин.
Шейдеры для OpenGL пишутся с использованием специального языка GLSL, на который имеется спецификация (доступна с сайта opengl.org). Мы будем использовать GLSL 3.30, эта версия GLSL вышла одновременно с выходом стандарта OpenGL 3.3.
Выше было отмечено, что шейдерная программа состоит из нескольких шейдеров для разных данных, в этом уроке нам понадобиться вершинный и фрагментный шейдеры. Вот код подходящего для нашей задачи вершинного шейдера:
#version 330 core
uniform mat4 projectionMatrix;
in vec3 position;
in vec3 color;
out vec3 fragmentColor;
void main(void)
{
gl_Position = projectionMatrix * vec4(position, 1.0);
fragmentColor = color;
}
Входные атрибуты обозначены как in, а выходные атрибуты как out, юниформы обозначаются как uniform. Выходные атрибуты предназначены для передачи данных из шейдера на следующий этап обработки данных, в нашем случае следующий этап это фрагментный шейдер:
#version 330 core
in vec3 fragmentColor;
out vec4 color;
void main(void)
{
color = vec4(fragmentColor, 1.0);
}
Здесь имеется выходной атрибут color, он определяет какого цвета будет пиксель на экране. Стоит также отметить что фрагментный шейдер получает интерполированные данные из предыдущего вершинного шейдера, каким образом данные будут интерполироваться можно указать отдельно для каждого атрибута, по-умолчанию это обычная линейная интерполяция. Таким образом, в случае с атрибутом fragmentColor, цвет меняется "линейно" от вершины к вершине.
После того как исходный код шейдеров написан их можно загрузить из программы и создать из них шейдерную программу. Исходный код шейдера это обычный текст, загрузить его можно как обычный текстовой файл, в исходника к статье для этого используется функция LoadFile, которая открывает файл, узнает его размер, выделяет под данные файла память и читает весь файл в память, после чего эти данные можно использовать.
Полностью создание шейдерной программы происходит следующим образом:
Для проверки статусов шейдера и шейдерной программы используется небольшая функция-обертка, которая в случае ошибки выводит текст этой ошибки в лог-файл:
GLint ShaderStatus(GLuint shader, GLenum param)
{
GLint status, length;
GLchar buffer[1024];
glGetShaderiv(shader, param, &status);
if (status != GL_TRUE)
{
glGetShaderInfoLog(shader, 1024, &length, buffer);
LOG_ERROR("Shader: %s\n", (const char*)buffer);
}
OPENGL_CHECK_FOR_ERRORS();
return status;
}
GLint ShaderProgramStatus(GLuint program, GLenum param)
{
GLint status, length;
GLchar buffer[1024];
glGetProgramiv(program, param, &status);
if (status != GL_TRUE)
{
glGetProgramInfoLog(program, 1024, &length, buffer);
LOG_ERROR("Shader program: %s\n", (const char*)buffer);
}
OPENGL_CHECK_FOR_ERRORS();
return status;
}
Вершинный буфер и его связь с вершинными атрибутами
Для хранения данных о вершинах геометрии в OpenGL существует специальный объект называемый Vertex Buffer Object, коротко VBO. VBO позволяет создать буфер в памяти видеокарты и поместить туда необходимые нам данные. Вершинный буфер создается с подсказкой, как часто мы будем менять данные в этом буфере.
Однако создания одного VBO недостаточно, необходимо создать еще один специальный объект Vertex Array Object, коротко VAO. VAO хранит связи между параметрами вершинных атрибутов и источниками данных в VBO. В VAO хранится из какого VBO какие атрибуты берутся, тип и размер этих атрибутов, смещение в буфере VBO до начала данных для этих атрибутов. Есть одно исключение - VBO для хранения индексов, к VAO можно присоединить только один такой VBO, но про индексный буфер будет рассказано в отдельном уроке.
Одно из удобств VAO, когда он будет настроен, не надо подключать различные VBO в которых хранятся атрибуты вершин, можно один раз подключить VAO и можно приступать к выводу геометрии.
В этом уроке мы используем VBO для хранения всего трех вершин для нашего треугольника, менять мы их не собираемся, поэтому данные поместим в VBO всего один раз, при инициализации OpenGL, там же создадим VAO:
const int vertexCount = 3;
const int vertexSize = 6 * sizeof(float);
static const float triangleMesh[vertexCount * 6] = {
-1.0f, -1.0f, -2.0f, 1.0f, 0.0f, 0.0f,
0.0f, 1.0f, -2.0f, 0.0f, 1.0f, 0.0f,
1.0f, -1.0f, -2.0f, 0.0f, 0.0f, 1.0f
};
GLuint meshVAO, meshVBO;
glGenVertexArrays(1, &meshVAO);
glBindVertexArray(meshVAO);
glGenBuffers(1, &meshVBO)
glBindBuffer(GL_ARRAY_BUFFER, meshVBO);
glBufferData(GL_ARRAY_BUFFER, vertexCount * vertexSize, triangleMesh, GL_STATIC_DRAW);
Передача атрибутов и юниформов в шейдер
Чтобы передать какие-то данные в шейдерную программу из нашего приложения необходимо использовать атрибуты и юниформы, общий алгоритм передачи данных такой:
# Получить индекс атрибута или юниформа в шейдере
# Передать по этому адресу данные из программы
В момент вызова функции glLinkProgram каждому атрибуту и юниформу назначается отдельный цифровой индекс, который используется для работы с ними из нашего приложения. Для получения индекса используются функции glGetAttribLocation и glGetUniformLocation для атрибутов и юниформов, соответственно.
У атрибутов есть одна особенность - мы можем использовать функцию glBindAttribLocation для назначения своих индексов атрибутам прежде чем вызвать функцию glLinkProgram. Можно и не устанавливать свои индексы, шейдер это сделает за нас, именно так и сделано в исходных кодах к этому уроку.
Когда шейдерная программа собрана, индексы интересующих нас атрибутов и юниформов получены и в VBO скопированы данные геометрии - можно настроить параметры вершинных атрибутов:
GLint positionLocation, colorLocation;
const int vertexOffsetPosition = 0;
const int vertexOffsetColor = 3 * sizeof(float);
positionLocation = glGetAttribLocation(shaderProgram, "position");
if (positionLocation != -1)
{
glVertexAttribPointer(positionLocation, 3, GL_FLOAT, GL_FALSE,
vertexSize, (const GLvoid*)vertexOffsetPosition);
glEnableVertexAttribArray(positionLocation);
}
colorLocation = glGetAttribLocation(shaderProgram, "color");
if (colorLocation != -1)
{
glVertexAttribPointer(colorLocation, 3, GL_FLOAT, GL_FALSE,
vertexSize, (const GLvoid*)vertexOffsetColor);
glEnableVertexAttribArray(colorLocation);
}
Матрицы
В данном уроке нам понадобится только одна матрица - перспективная матрица проекции. Она нам необходима, чтобы переводить позиции вершин в однородные координаты (clip space). Раньше установка матрицы проекции делалась достаточно просто, сначала мы говорили, что хотим поменять матрицу проекции командой glMatrixMode(GL_PROJECTION), а потом указывали параметры матрицы проекции, например для создания перспективной матрицы проекции использовалась функция gluPerspective(fov, aspect, znear, zfar).
В OpenGL версии 3.0 и выше отменили работу со стеком матриц OpenGL, теперь необходимо вручную создавать матрицы и передавать их в шейдер для дальнейшего использования. Создать матрицу, подобную той что делает gluPerspective, просто:
void Matrix4Perspective(float *M, float fovy, float aspect, float znear, float zfar)
{
float f = 1 / tanf(fovy / 2),
A = (zfar + znear) / (znear - zfar),
B = (2 * zfar * znear) / (znear - zfar);
M[ 0] = f / aspect; M[ 1] = 0; M[ 2] = 0; M[ 3] = 0;
M[ 4] = 0; M[ 5] = f; M[ 6] = 0; M[ 7] = 0;
M[ 8] = 0; M[ 9] = 0; M[10] = A; M[11] = B;
M[12] = 0; M[13] = 0; M[14] = -1; M[15] = 0;
}
В итоге в массиве M у нас будет готовая перспективная матрица. Именно эту матрицу мы будем передавать в шейдер как юниформ projectionMatrix:
float projectionMatrix[16];
GLint projectionMatrixLocation;
const float aspectRatio = (float)window->width / (float)window->height;
Matrix4Perspective(projectionMatrix, 45.0f, aspectRatio, 0.5f, 5.0f);
projectionMatrixLocation = glGetUniformLocation(shaderProgram, "projectionMatrix");
if (projectionMatrixLocation != -1)
glUniformMatrix4fv(projectionMatrixLocation, 1, GL_TRUE, projectionMatrix);
Правила перевода из однородных координат в экранные задает функция glViewport, от которой в итоге зависит в каком месте окна будет выводиться вся графика.
Подробнее про матрицы и трансформации будет рассказано в следующем уроке, где мы подробней остановимся на системе преобразования вершинных координат принятой в OpenGL и других GAPI.
Вывод треугольника
Наконец мы подошли к последнему шагу нашего урока - вывод треугольника на экран. Совместив все предыдущие занния мы можем создать шейдер, загрузить его, установить нужные параметры, скопировать данные геометрии в буфер видеокарты, установить матрицу и наконец приступать к рендерингу треугольника:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glUseProgram(shaderProgram);
glBindVertexArray(meshVAO);
glDrawArrays(GL_TRIANGLES, 0, vertexCount);
Использование VAO
Согласно стандарту OpenGL версии 3.3 операции над вершинными атрибутами без активного VAO более не допускаются (секция E стандарта). Указано, что VAO по-умолчанию (индекс 0) считается устаревшим и при использовании функции glVertexAttribPointer без текущего активного VAO будет выдана ошибка INVALID_OPERATION. На момент написания урока в драйверах ATI именно так и сделано, однако nVidia все еще позволяет пользователю не создавать VAO.
Полезные ссылки
Исходный код
Доступ к исходному коду уроку с проектом для MSVC можно получить двумя способами:
#OpenGL, #уроки
26 октября 2010
(Обновление: 5 мая 2020)
Комментарии [64]