Как стать автором
Обновить

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

Программирование *C++ *Работа с 3D-графикой *Разработка игр *CGI (графика) *


Всем привет. Все кто хоть немного разбирался в теме 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);
}
Теги:
Хабы:
Всего голосов 53: ↑52 и ↓1 +51
Просмотры 34K
Комментарии Комментарии 25