Суперсовременный OpenGL. Часть 1

uiwu2kjzkvznzwubjaqnk7lkoz0.jpeg

Всем привет. Все кто хоть немного разбирался в теме 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 при ошибках и дебаг сообщениях, уровень которых мы сможем настроить.

t-m71nmkhv4-khfdlpb99wekjxq.jpeg

Нам надо вызвать всего две функции для включения: 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 добавили некоторые новые типы шейдеров, а именно шейдеры геометрии, теселяции и расчетов (о них я расскажу в следующей части).

lgorrggfshrf8xdb6vbwu4fw-so.jpeg
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);
}


Если вам понравилось, ждите продолжение:)

© Habrahabr.ru