Суперсовременный OpenGL. Часть 1
Всем привет. Все кто хоть немного разбирался в теме OpenGL знают, что существует большое количество статей и курсов по этой теме, но многие не затрагивают современный API, а часть из них вообще рассказывают про glBegin и glEnd. Я постараюсь охватить некоторые нюансы нового API начиная с 4-й версии.
На этот раз я попробую написать интересную и познавательную статью, а что получилось — решать добрым хабравчанинам. Прошу простить меня за мою плохую грамматику (буду благодарен за исправления).
Если вам понравится, напишу про оптимизацию OpenGL и уменьшение DrawCall’ов.
Приступим!
Что будет в этой статье — функционал современного OpenGL
Чего не будет в этой статье — современные подходы к рендерингу на OpenGL
- Direct State Access
- Debug
- Separate Shader Objects
- Texture arrays
- Texture view
- Single buffer for index and vertex
- Tessellation and compute shading
- Path rendering
DSA (Direct State Access)
Direct State Access — Прямой доступ к состоянию. Средство изменения объектов OpenGL без необходимости привязывать их к контексту. Это позволяет изменять состояние объекта в локальном контексте, не затрагивая глобальное состояние, разделяемое всеми частями приложения. Это также делает API-интерфейс немного более объектно-ориентированным, поскольку функции, которые изменяют состояние объектов, могут быть четко определены. Вот что нам говорит OpenGL Wiki.
Как мы знаем, OpenGL — это API-интерфейс с множеством переключателей — glActiveTexture, glBindTexture и т.д.
Отсюда у нас возникают некоторые проблемы:
- Селектор и текущие состояния могут вносить более глубокое изменение состояния
- Может потребоваться привязать / изменить активный юнит, чтобы установить флильтр для текстур
- Управление состоянием становится проблематичным в следствии чего растет сложность приложения
- Неизвестное состояние приводит к дополнительным настройкам
- Попытки сохранить/восстановить состояние могут быть проблематичны
Что же предложили нам Khronos group и как же помогает DSA?
- Добавляет функции, которые работают непосредственно с объектом / объектами
- Устанавливает фильтр текстуры для указанного объекта текстуры, а не текущего
- Привязывает текстуру к конкретному юниту, а не к активному
- Добавляет очень большое количество новых функций
- Покрывает вещи вплоть до OpenGL 1.x
- Добавляет дополнительные функции
В теории DSA может помочь свести количество операций не относящихся к отрисовки и меняющих состояние к нулю… Но это не точно.
Теперь я вкратце пробегусь по некоторым новым функциям, подробно останавливаться на параметрах не буду, оставлю линки на вики.
- glCreateTextures заменяет glGenTextures + glBindTexture (инициализация).
Было:
Стало:glGenTextures(1, &name); glBindTexture(GL_TEXTURE_2D, name);
glCreateTextures(GL_TEXTURE_2D, 1, &name);
- glTextureParameterX эквивалент glTexParameterX
Теперь же мы это напишем так:glGenTextures(1, &name); glBindTexture(GL_TEXTURE_2D, name); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, width, height); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
glCreateTextures(GL_TEXTURE_2D, 1, &name); glTextureParameteri(name, GL_TEXTURE_WRAP_S, GL_CLAMP); glTextureParameteri(name, GL_TEXTURE_WRAP_T, GL_CLAMP); glTextureParameteri(name, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTextureParameteri(name, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTextureStorage2D(name, 1, GL_RGBA8, width, height); glTextureSubImage2D(name, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
- glBindTextureUnitзаменяет glActiveTexture + glBindTexture
Вот как мы делали:
Теперь:glActiveTexture(GL_TEXTURE0 + 3); glBindTexture(GL_TEXTURE_2D, name);
glBindTextureUnit(3, name);
Так же изменения коснулись glTextureImage, он более не используется и вот почему:
glTexImage довольно небезопасная, очень легко получить невалидные текстуры, потому что функция работает без проверки при вызова, а все значения проверяются драйвером во время рисования. Для ее замены была добавлена glTexStorage.
glTexStorage предоставляет способ создания текстур с проверками, выполняемыми во время вызова, что сводит количество ошибок к минимуму. Хранилище текстур решает большинство, если не все проблемы, вызываемые изменяемыми текстурами, хотя неизменяемые текстуры — более надёжно.
Изменения затронули и буфер кадров:
Это не все измененные функции. Следующие на очереди — функции для буферов:
Вот список того, что сейчас входит в поддержку DSA:
- Vertex array objects
- Framebuffer objects
- Program objects
- Buffer objects
- Matrix stacks
- Много устаревших вещей
Debug
С версии 4.3 был добавлен новый функционал для дебага, на мой взгляд очень полезный и удобный. Теперь OpenGL будет вызывать наш callback при ошибках и дебаг сообщениях, уровень которых мы сможем настроить.
Нам надо вызвать всего две функции для включения: glEnable& glDebugMessageCallback, проще некуда.
glEnable(GL_DEBUG_OUTPUT);
glDebugMessageCallback(message_callback, nullptr);
Теперь напишем callback функцию для получения месседжа:
void callback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, GLchar const* message, void const* user_param)
{
auto source_str = [source]() -> std::string {
switch (source)
{
case GL_DEBUG_SOURCE_API: return "API";
case GL_DEBUG_SOURCE_WINDOW_SYSTEM: return "WINDOW SYSTEM";
case GL_DEBUG_SOURCE_SHADER_COMPILER: return "SHADER COMPILER";
case GL_DEBUG_SOURCE_THIRD_PARTY: return "THIRD PARTY";
case GL_DEBUG_SOURCE_APPLICATION: return "APPLICATION";
case GL_DEBUG_SOURCE_OTHER: return "OTHER";
default: return "UNKNOWN";
}
}();
auto type_str = [type]() {
switch (type)
{
case GL_DEBUG_TYPE_ERROR: return "ERROR";
case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: return "DEPRECATED_BEHAVIOR";
case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: return "UNDEFINED_BEHAVIOR";
case GL_DEBUG_TYPE_PORTABILITY: return "PORTABILITY";
case GL_DEBUG_TYPE_PERFORMANCE: return "PERFORMANCE";
case GL_DEBUG_TYPE_MARKER: return "MARKER";
case GL_DEBUG_TYPE_OTHER: return "OTHER";
default: return "UNKNOWN";
}
}();
auto severity_str = [severity]() {
switch (severity) {
case GL_DEBUG_SEVERITY_NOTIFICATION: return "NOTIFICATION";
case GL_DEBUG_SEVERITY_LOW: return "LOW";
case GL_DEBUG_SEVERITY_MEDIUM: return "MEDIUM";
case GL_DEBUG_SEVERITY_HIGH: return "HIGH";
default: return "UNKNOWN";
}
}();
std::cout << source_str << ", "
<< type_str << ", "
<< severity_str << ", "
<< id << ": "
<< message << std::endl;
}
Так же мы можем настроить фильтр при помощи glDebugMessageControl. Фильтр может работать в режиме фильтрации по источнику/типу/важности или набора сообщений с использованием их идентификаторов.
Фильтр сообщений в определенном скоупе:
glPushDebugGroup( GL_DEBUG_SOURCE_APPLICATION, DEPTH_FILL_ID, 11, "Depth Fill”); //Добавляем маркер
Render_Depth_Only_Pass(); //Выполняем рендеринг
glPopDebugGroup(); //Убираем маркер
Очень полезно будет отключить асинхронный вызов, что б мы могли определить порядок вызовов функций, а так же найти место ошибки в стеке при дебаге. Это делается довольно просто:
glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
Стоит помнить, что небезопасно вызывать OpenGL или функции окон в функции обртаного вызова, а так же обратные вызовы небесплатные и не стоит оставлять их включенными в релизной сборке.
Болеее подробно про эти и другие штучки — дрючки, можно почитать здесь.
SSO (Separate Shader Objects)
Когда-то OpenGL работал как «фиксированный конвейер» — это означало, что ко всем передаваемым на визуализацию данным применялась заранее запрограммированная обработка. Следующим шагом было «програмируемый конвейер» — где программируемая часть осуществляет шейдеры, написан в GLSL, классический GLSL программа состояла из вершинного и фрагментного шейдера, но в современном OpenGL добавили некоторые новые типы шейдеров, а именно шейдеры геометрии, теселяции и расчетов (о них я расскажу в следующей части).
SSO позволяют нам изменять этапы шейдера на лету, не связывая их заново. Создание и настройка простого программного конвейера без отладки выглядит следующим образом:
GLuint pipe = GL_NONE;
// Create shaders
GLuint fprog = glCreateShaderProgramv( GL_FRAGMENT_SHADER, 1, &text);
GLuint vprog = glCreateShaderProgramv( GL_VERTEX_SHADER, 1, &text);
// Bind pipeline
glGenProgramPipelines( 1, &pipe);
glBindProgramPipelines( pipe);
// Bind shaders
glUseProgramStages( pipe, GL_FRAGMENT_SHADER_BIT, fprog);
glUseProgramStages( pipe, GL_VERTEX_SHADER_BIT, vprog);
Как мы видим glCreateProgramPipelines генерирует дескриптор и инициализирует объект, glCreateShaderProgramv генерирует, инициализирует, компилирует и связывает шейдерную программу с использованием указанных источников, а glUseProgramStages присоединяет этапы программы к объекту конвейера. glBindProgramPipeline — связывает конвейер с контекстом.
Но есть один нюанс, теперь входные и выходные параметры шейдеров должны совпадать. Мы можем объявить входные/выходные параметры в одном и том же порядке, с одинаковыми именами, либо мы делаем их местоположение явно совпадающим с помощью квалификаторов.
Я рекомендую последний вариант, это позволит нам настроить четко определенный интерфейс, а также проявить гибкость в отношении имен и порядка.
В качестве обеспечения более строгого интерфейса нам также нужно объявить встроенные блоки ввода и вывода, которые мы хотим использовать для каждого этапа.
Встроенные интерфейсы блоков определены как (из вики):
Vertex:
out gl_PerVertex
{
vec4 gl_Position;
float gl_PointSize;
float gl_ClipDistance[];
};
Tesselation Control:
out gl_PerVertex
{
vec4 gl_Position;
float gl_PointSize;
float gl_ClipDistance[];
} gl_out[];
Tesselation Evaluation:
out gl_PerVertex {
vec4 gl_Position;
float gl_PointSize;
float gl_ClipDistance[];
};
Geometry:
out gl_PerVertex
{
vec4 gl_Position;
float gl_PointSize;
float gl_ClipDistance[];
};
Пример повторного объявления встроенного модуля и использование attribute location в обычном вершинном шейдере:
#version 450
out gl_PerVertex { vec4 gl_Position; };
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 color;
layout (location = 0) out v_out
{
vec3 color;
} v_out;
void main()
{
v_out.color = color;
gl_Position = vec4(position, 1.0);
}
Если вам понравилось, ждите продолжение:)