Всем привет. Все кто хоть немного разбирался в теме 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);
}


Ссылка на вторую часть статьи

Комментарии (24)


  1. kirivasile
    24.06.2019 13:41

    Можно вопрос оффтопом:
    А какие бы посоветовали туториалы по последнему OpenGL?
    А то немного писал на старых версиях, и мне кажется, что во многом я использую очень старый функционал.


    1. kiwhy Автор
      24.06.2019 13:43

      ogldev.atspace.co.uk
      learnopengl.com

      Вот эти ресурсы дают неплохую базу, так же на habr есть перевод части уроков learnopengl
      А касательно современных штук: документация от NVidia и англоязычные статьи.
      Я постараюсь выпустить ряд статей по новым фичам и техникам (в меру своих знаний и найденного материала)


      1. UberSchlag
        24.06.2019 17:52

        Уточню, по поводу переводов learnopengl.com. Переведены все же все уроки из основной, технической секции. Секция, посвященная сборке своего арканоида не переведена, это да.


        1. kiwhy Автор
          24.06.2019 17:57

          Извеняюсь, последний раз когда смотрел (не было PBR)


      1. Bombus
        24.06.2019 19:50

        Спрошу здесь, т.к. этот ресурс указали.
        Ни у кого нет проблем с доступом к learnopengl.com? У меня без дополнительных движений данный сайт не открывается (Firefox, Ubuntu, Ростелеком). Приходится или в кеш гугла заглядывать или еще чего. У кого-нибудь открывается этот туториал нормально?


        1. tbl
          25.06.2019 01:26

          Аналогично, ростелеком, открывается только через proxy или vpn.

          Похоже, это опять их DPI-решение чудит и рубит не понравившийся https

          P.S.: Создал обращение, посмотрим, что ответят.

          Заголовок спойлера


          1. mmmm1998
            25.06.2019 11:10

            Так он просто забанен РКН по IP.

            [10:47:44]$ ping learnopengl.com
            PING learnopengl.com (128.199.49.46) 56(84) bytes of data.
            64 bytes from 128.199.49.46 (128.199.49.46): icmp_seq=1 ttl=59 time=80.1 ms
            64 bytes from 128.199.49.46 (128.199.49.46): icmp_seq=2 ttl=59 time=78.8 ms
            ^C
            — learnopengl.com ping statistics — 2 packets transmitted, 2 received, 0% packet loss, time 1000ms
            rtt min/avg/max/mdev = 78.873/79.513/80.154/0.699 ms



            1. tbl
              25.06.2019 12:15

              а, это телеграмные блокировки, вот ведь паразиты


        1. CEHEKA
          25.06.2019 11:10

          Не открывается.


  1. kiwhy Автор
    24.06.2019 13:44

    Ребят, если у кого-то есть желание помочь с форматированием и правками в следующих статьях, очень прошу — напишите :)


  1. vyo
    24.06.2019 16:07

    Больной вопрос (надеюсь, не сильно оффтоп) — есть ли хорошие туториалы по Vulkan, где не требуется предварительное знание OpenGL, чтобы не учить первый полностью в придачу? В идеале, с Vulkan-hpp (даже для него доков мало несколько).
    А то что не открою — "возьмите программиста с опытом работы с OpenGL".


    1. kiwhy Автор
      24.06.2019 16:16

      vulkan-tutorial.com но они на англ, я могу написать авторам и если людям будет интересно — перевести.
      Касательно OpenGL — он дает базу, понимание как происходит создание камеры, что такое матрицы Model/View/Projection, но это все можно выучить и на DirectX, и на Vulkan. Материала касательно OpenGL намного больше из-за чего и понять/вычить будет быстрее. По вулкану материалов очень мало, нужно сидеть и смотреть какие проекты есть в открытом доступе, изучать код движка и тд.
      Надеюсь понятно обьяснил :)


      1. sakhapovi
        24.06.2019 16:42

        Перевод был, но сайт закрылся внезапно. Кстати, довольно хороший: web.archive.org/web/20180103131302/http://vulkanapi.ru/


        1. kiwhy Автор
          24.06.2019 17:19

          На самом деле, перевод — не проблема.


    1. sakhapovi
      24.06.2019 16:28
      +1

      Скажем так — Vulkan сам по себе не требует предварительных знаний OpenGL или DirectX, но с практикой в одном из них, можно серьезно улучшить воспринимаемость изучаемых по этой тематике материалов. Как по мне, идеальным подходом к изучению Vulkan без большого опыта с OpenGL и DirectX будет пробег по знаменитому в кругах туториалу vulkan-tutorial.com с написанием кодовой базы вручную(крайне желательно). Кода для вывода примитива с текстурированием получится много, усидчивость приветствуется. После этого скачать/купить книгу Vulkan Programming Guide: The Official Guide to Learning Vulkan. В ней детально описывается сам API, но нет никакой практики, её как раз можно будет взять из кода туториала выше. И вот так, глава за главой, поглядывая в исходный код будет проходить изучение. Vulkan — очень сложный, но он логичный. Поэтому хорошо бы было вести mind-map изученного материала, иначе быстро будете терять ощущение прогресса и могут вовсе руки опуститься. В обилии структур и привязок вулкан превзойдет возможно всё, что вы могли до этого видеть, в этом будет сложно разобраться. Пожелаю удачи!


      1. vyo
        24.06.2019 18:12

        В своё время пытался его освоить по https://vulkan-tutorial.com, но загнулся из-за попыток одновременно понять vulkan-hpp.
        Благодарю за совет сначала пробовать писать код, а лишь потом смотреть примеры.


        1. kiwhy Автор
          24.06.2019 18:34

          для Нормального понимания, нужно разбираться в основах работы vulkan, так как он работает на низком уровне и нужно делать много того, что за вас делал opengl


  1. truthfinder
    24.06.2019 18:14
    +2

    Автор, нормально пишешь. Про оптимизацию тоже смело пиши.


  1. mikechips
    25.06.2019 00:12

    Изображение на обложке так и напоминает тех самых товарищей, которые только три дня знают синтаксис языка и уже норовят похвалиться каждому встречному :)


  1. iga2iga
    25.06.2019 05:59

    Как теперь более правильно стримить текстуры, например для показа видео? Проверки, это хорошо, но иногда нужна и скорость… Кстати, заметил такую разницу между NV и AMD — у AMD крайне медленно работает glTexImage2D в цикле, даже если надо просто обновить уже имеющиеся данные текстуры без изменения размера и типа пикселей, там как будто текстура полностью и честно пересоздается каждый раз с очисткой и перевыделением памяти и решается это только через glTexSubImage2D, но есть моменты когда нужен именно glTexImage2D (вдруг прилетит кадр не такой как предыдущий). На карточках NV видимо имеется некий спидхак и наличие glTexImage2D с теми же самыми параметрами с которыми была вызвана в первый раз для этой же текстуры вообще никак не влияет на производительность.


    1. kiwhy Автор
      25.06.2019 11:29

      Это логично, так как при вызове glTexImage2D мы пересоздаем текстуру, у NVidia скорее всего стоит оптимизация какая-то (он возможно не подчищает и не перевыделяет память, так как создаете с теме же параметрами). В данном случае, вы создаете так называемые mutable texture.
      Я немного не понял, про «не такой как предидущий» кадр…
      А для ускорения используется PBO он убирает одно копирование данных. И запись из PBO идет через DMA.
      www.songho.ca/opengl/gl_pbo.html#unpack — тут хороший пример


      1. iga2iga
        25.06.2019 12:24

        По поводу «не такой как предыдущий» — имелось ввиду, что когда прилетают кадры видео ролика/карты захвата/веб-камеры и пр. их обрабатывает единственная функция у меня, которая и использует glTexImage2D, которая в свою очередь снимает кучу проблем с выяснением «а какой по размеру был предыдущий кадр (смена видеоролика например), какой формат пикселей был у предыдущего кадра, надо ли пересоздать формат/размер текстуры через glTexImage2D с нулевым пойнтером. К тому же у меня абсолютно всеядный плеер по yuv форматам, коих просто куча, опять же благодаря простоте использования glTexImage2D. Использую ffmpeg разумеется и получаю непосредственно распакованный кадр в yuv формате любом (конвертирование шейдером). Пробовал использовать glTexSubImage2D, но это надо сохранять данные каждого полученного кадра и каждый последующий сравнивать с предыдущим по формату/размеру и т.д., чтобы в случае чего пересоздать текстуру через glTexImage2D с нулевым пойнтером.
        По поводу PBO — видел, пробовал. В моем случае ускоряет ровно в 2 раза при загрузке кадра 8k (7680x4320), но все упирается в софтовое копирование данных в маппированную память GPU.
        По ссылке — это void updatePixels(GLubyte* dst, int size), т.е. мне все равно надо вручную копировать полученный из ffmpeg кадр куда-то, а в моем случае еще и до 3х раз при наличии 3х битовых полей у yuv кадра. В итоге разница с простецким glTexImage2D даже хоть и 2 раза, но 8k 60fps не тянет ни то ни это… максимум 30.
        зы тестировалось на gtx1080 и Vega64


        1. kiwhy Автор
          25.06.2019 13:30

          Я постараюсь поискать ответ, но не обещаю, что смогу помочь


          1. iga2iga
            25.06.2019 20:15

            Вообще сейчас вот немного побаловался с PBO и таки удалось догнать сэмулированный через bmp по объему данных кадр якобы 8k yuv420p (7680х1620х32bit) до 110 к/c стиминг+отрисовка… Немного помогло кастомное SSE копирование ещё. Это 9мс уже с отрисовкой, ну 1-2 еще на преобразования, вроде как укладываемся в 16мс. Надо будет покопать в эту сторону…