OpenGL: Сглаживание краев. ARB-Multisample

Наконец-то появилось время реализовать ранее написанный obj-ридер в OpenGL. В процессе выяснилось, что с момента моего последнего общения с OpenGL прошло слишком много времени. Совершенно не устраивал результат из-за гадских «ступенек». Мне нужно сглаживание…

Проблема

Открыть OBJ-модель и отобразить в OpenGL проблем не вызвало. Проблема оказалась в том, что моих знаний не хватало, чтобы избавится от этого убогого «ступенчатого» вида

Поэтому, погрузившись в дебри интернета, выяснил, что по поводу сглаживания есть глубокомысленные отсылки к расширениям ARB явно что-то знающих людей и нет ни одного готового рецепта. Для Delphi ничего внятного нет и в помине. Те рецепты, которые нашёл, сглаживания мне не давали.

Но я всё ж таки подключил и включил. Но, обо всём по порядку.

Постановка задачи

Необходимо научиться отображать 3D-объект со сглаживанием без использования движков, сторонних библиотек и последних версий Delphi (Skia, знаете, тот ещё монстр). Также, хочется рисовать не в окне, специально созданном для контекста OpenGL, а на какой-нибудь скромной панельке внутри формы.

Чтение информации о расширениях ARB и вопросы к ИИ выработали стойкое убеждение, что без шейдеров не обойтись. Что все старые функции OpenGL канули в лету и, даже если всё таки включить ARB, они работать не будут. Мне это кажется крайне нелогичным, поэтому хочу опровергнуть.

Почему не движки, библиотеки и последние версии?

Что плохого в движках? Ничего. Просто они большие, и для ряда задач не нужны. И потом, они меняются, и порой весьма кардинально. И в один прекрасный момент можно просто не скомпилировать проект. На текущий момент мне известен только один выживший, это GLScene. Но он вызывает у меня крайне противоречивые чувства и плохие воспоминания.

Не хочется таскать с проектом файлы библиотек. Проект желательно иметь в одном каталоге, без привязок, настроек путей, наличия определённых компонент. Чтобы открыл, скомпилировал и улыбнулся.

И, наконец, почему не в последних версиях Delphi. Потому что, во-первых, они есть далеко не у каждого. Во-вторых, лично у меня последняя Delphi (сейчас это XE 12) — основной рабочий инструмент, где настроены пути, переписаны модули, где всё не так, как в кристально чистой, только что установленной версии. Поэтому, для экспериментов и статей у меня есть XE 7, которая никак не пересекается с 12-ой.

Плюс ко всему, мне кажется, что понимая, как всё это работает, легче использовать движки и прочие прелести. Ничто не совершенно. Когда понимаешь, в чём может быть проблема, можешь залезть и сам починить. Потом написать автору — дескать, посмотри в этом месте, возможно бага.

[свернуть]

Создание контекста GL

Стандартный порядок действий для инициализации работы в OpenGL такой. Нам нужно получить контекст устройства, на котором собираемся рисовать, настроить формат пикселя и получить контекст GL.

Предположим, что у нас есть класс, который отвечает за отрисовку в OpenGL. Мы каким-то образом, например, через конструктор, передали Handle того окна (панели), на котором хотим рисовать и после этого вызываем процедуру инициализации.

Метод инициализации очень прост и буквально воспроизводит порядок действий, описанный в первом абзаце:

Функция wglCreateContext создает новый контекст отрисовки OpenGL, который подходит для рисования на указанном контексте устройства. Контекст отрисовки имеет тот же формат пикселей, что и контекст устройства.

Функция wglMakeCurrent делает заданный контекст отрисовки OpenGL текущим контекстом отрисовки вызывающего потока. Все последующие вызовы OpenGL, выполняемые потоком, рисуются на заданном контексте устройства.

Метод для завершения работы с OpenGL:

Пояснения к записи wglMakeCurrent(0, 0): «Если hglrc имеет значение NULL, функция делает текущий контекст отрисовки вызывающего потока не актуальным. В этом случае hdc игнорируется«. То есть в первом параметре может быть всё, что угодно.

Если всё сделано в одном потоке, делать этот вызов необязательно, wglDeleteContext сделает контекст не текущим и так. Но мы перестраховываемся.

Настройку формата пикселей можно сделать следующей процедурой:

procedure SetupPixelFormat(DC: HDC)

[свернуть]

Функция ChoosePixelFormat пытается сопоставить соответствующий формат пикселей, поддерживаемый контекстом устройства, с заданной спецификацией формата пикселей. Проще говоря, мы спрашиваем, ты такое потянешь? И если да, то вернёт что-то, отличное от нуля.

И всё работает. Но видя такое, опускаются руки…

Мультисэмплинг и сглаживание

Для того, чтобы получить сглаживание, нам нужен мультисэмплинг. Знающие люди говорят, что все старые методики сглаживания не работают. Например, glEnable(GL_MULTISAMPLE) ни на что не влияет. И они действительно не работают. И не факт, что когда-либо работали. Для реализации нормального сглаживания, говорят знающие люди, необходимо использовать ARB-Multisample.

Описание технологии мультисэмплинга, и что это такое, в большинстве случаев общие слова и перепечатка друг у друга. Но вот тут мне понравилось: Как работает рендеринг в 3D-играх: сглаживание. Несмотря на большое количество брюзжащих комментариев.

Если тезисно, то: 1) суперсэмплинг (Supersampling anti-aliasing, SSAA) — это рендер одной и той же сцены в разных разрешениях и подсчёт среднего арифметического для каждого пикселя. 2) мультисэмплинг (Multisample anti-aliasing, MSAA) — это почти как SSAA, но только работает он на краях проблемных областей. Край проблемной области — это края примитива на фоне, ступенчатая 🤬 грань куба. Всё на самом деле несколько сложнее, но не об том статья.

Таким образом, чтобы получить сглаживание, нам надо, чтобы движок мог анализировать более одного сэмпла.

Включить Multisample оказалось задачей нетривиальной. Дело в том, что описанные выше функции ChoosePixelFormat и wglCreateContext появились давно и абсолютно нерасширяемы. В структуре PIXELFORMATDESCRIPTOR нет поля, в котором можно было бы указать количество сэмплов для сглаживания. Чтобы получить возможность использовать другие функции, взамен указанных выше, нам нужны ARB-расширения.

ARB-расширения OpenGL

Неуёмный креатив производителей видеокарт приводит к появлению новых плюшек. В архитектуре OpenGL эти плюшки доступны как расширения. Есть расширения, которые принадлежат только конкретному производителю. Например, расширения от NVIDIA имеют в названии NV (WGL_NV_DX_interop).

Расширения, имеющие в названии ARB (WGL_ARB_pixel_format), одобрены Советом по обзору архитектуры OpenGL и рекомендованы к использованию. Совет почил в бозе, но ARB мы верим. Вот тут ребята разъясняют про расширения: Что такое расширения OpenGL и каковы преимущества/компромиссы их использования?

Невозможно описать все псевдонимы функций всех расширений. Однако, есть возможность получить функцию по названию. Этим целям служит функция wglGetProcAddress:

ProcName — это название функции. То есть, если мне нужна функция wglGetExtensionsStringARB, то я так и попрошу:

Чтобы использовать эти функции, разработчик должен знать их псевдонимы. Потому что функция wglGetProcAddress возвращает просто указатель. И что там под этим указателем, должен рассказать тип переменной, которой мы этот указатель присваиваем.

Функция wglGetExtensionsStringARB, которую мы только что запросили, возвращает строку с перечнем расширений для заданного контекста устройства. Расширения разделены пробелом. Строка заканчивается символом #0. Вид возвращаемой строки примерно такой:

‘WGL_EXT_depth_float WGL_ARB_buffer_region WGL_ARB_extensions_string WGL_ARB_make_current_read WGL_ARB_pixel_format WGL_ARB_pbuffer WGL_EXT_extensions_string WGL_EXT_swap_control WGL_ARB_multisample WGL_ARB_pixel_format_float WGL_ARB_framebuffer_sRGB WGL_ARB_create_context WGL_ARB_create_context_profile WGL_EXT_pixel_format_packed_float WGL_EXT_create_context_es_profile WGL_EXT_create_context_es2_profile WGL_NV_DX_interop WGL_NV_DX_interop2 WGL_ARB_robustness_application_isolation WGL_ARB_robustness_share_group_isolation WGL_ARB_create_context_robustness WGL_ARB_context_flush_control ‘

Давайте напишем функцию, которая будет определять наличие расширения в системе по имени:

Есть более навороченный вариант функции. Подсмотрен в Skia:

HasExtension

[свернуть]

Пока всё хорошо. Но есть одна проблема. В стандартной поставке Delphi нет ни типов, ни переменных для таких функций. Их надо прописывать руками.

Или нет?

Модуль dglOpenGL

Хотелось бы иметь некий модуль, в котором прописан максимально возможный набор псевдонимов функций. Потому что, в противном случае, придётся каждую функцию объявлять самому. В стандартной поставке Delphi такого нет. Зато есть в сети.

Это довольно популярный модуль dglOpenGL.pas. Немецкие программисты были недовольны родными заголовочными модулями Delphi и написали свой. Модуль содержит большой набор псевдонимов функций различных расширений и позволяет их подключать частями.

Возможно, модуль устарел, скажете вы. Вовсе нет, не соглашусь я. На текущий момент последняя версия OpenGL — 4.6, выпущена 31 июля 2017 года. Версия 4.6. в модуле представлена.

Чтобы его использовать, надо убрать из предложения uses модули Winapi.OpenGL, Winapi.OpenGLext и вместо них объявить dglOpenGL. В этом случае инициализация и завершение работы с GL будет выглядеть так:

Как видим, всё предельно просто. Несколько строк и всё работает.

Модуль имеет лицензию MPL 2.0. Если это каким-то образом не устраивает, модуль можно использовать как справочный материал и переносить нужное к себе. Грамотный копипаст — основа мироздания.

Включаем ARB-Multisample

По сути, нам нужно иметь больше настроек для формата пикселей и функцию, которая сможет создать контекст GL, понимая эти настройки.

Чтобы определить, существует ли вообще ARB-Multisample в системе, необходимо запросить расширение WGL_ARB_multisample. Если такой существует, нам понадобится расширение WGL_ARB_create_context, которое реализуется функцией wglCreateContextAttribsARB, и WGL_ARB_pixel_format, чтобы получить функцию wglChoosePixelFormatARB.

Проблема состоит в том, что получить эти функции можно только при наличии созданного контекста OpenGL. Казалось бы, вначале создали простой контекст. Получили нужные функции, отсоединили контекст. И создали уже правильный, с новыми настройками. Но для Windows такой финт не получится. Мы можем задать формат пикселей для окна только один раз. Очевидно, что для инициализации промежуточного контекста, нам придётся создавать какое-то временное окно.

Сценарий решения проблемы описан тут — OpenGL: Proper Context Creation.

Подготовка

Опишем директиву, переключающую на использование либо dglOpenGL, либо родные модули Delphi. Связано с тем, что кто-то может иметь неприязнь к чему-то стороннему, недолюбливать лицензию MPL, или же поставлена задача не использовать ничего стороннего.

Если мы не используем dglOpenGL, то заберём из него всё то полезное, что нам нужно и оформим следующим блоком

dglOpenGL

[свернуть]

Добавим класс исключения. Чтобы идентифицировать в будущем, что это точно наше.

Временное окно и фиктивный контекст

Чтобы не испортить формат пикселя основного окна, в котором собираемся рисовать, создадим временное окно. В нём создадим временный контекст, способом, который описан выше. При успешном создании контекста, получим функции нужных нам расширений. Затем уничтожим и контекст, и окно.

Структура TPixelFormatDescriptor уже создана в вызывающей стороне. Листинг с заполнением структуры и вызовом будет ниже.

Настройка формата пикселя

В настройках формата пикселей нам не хватало только указания количества сэмплов для сглаживания. Теперь у нас такая возможность есть посредством функции wglChoosePixelFormatARB.

Вместо того, чтобы брать фиксированную структуру PFD, функция берет список атрибутов и значений. Многие из этих атрибутов имеют прямые аналоги полей структуры PFD.

hDC: HDC
Контекст устройства, с которым связан контекст GL.
const piAttribIList: PGLint
Список целочисленных атрибутов. Каждые два элемента в списке — это пара атрибут/значение. Атрибут «0» означает конец списка, после него не требуется значение.
Допустимо значение NIL.
const pfAttribFList: PGLfloat
Cписок атрибутов с плавающей точкой. Каждые два элемента в списке — это пара атрибут/значение.
nMaxFormats: GLuint
Максимальное количество форматов, которые будут сохранены в piFormats.
piFormats: PGLint
Список форматов. Функция может возвращать несколько форматов. Их порядок — от наилучшего соответствия к худшему.
nNumFormats: PGLuint
Возвращаемое значение, означающее, сколько записей на самом деле в списке piFormats.

Если функция возвращает FALSE, то код не смог найти подходящий формат пикселей. Для выяснения причин необходимо получить код последней ошибки Windows.

Непосредственно установки формата пикселя тут нет. Она происходит в листинге ниже.

В функцию передаётся заранее сформированный массив атрибутов. Это неизменная часть настройки формата пикселя, дублирующая PFD. Мы просто дописываем в настройку количество сэмплов, которые хотим использовать.

Создание контекста GL с мультисэмплом

За создание контекста OpenGL с дополнительными атрибутами отвечает функция wglCreateContextAttribsARB.

Поле hShareContext является специальным. Его назначение пока для меня туманно, поэтому равно нулю.

В поле attribList я указал версию 3.1:

Указывая более высокую версию, терял возможность рисовать старыми функциями. То есть контекст создавался без ошибок, что-то там происходило, но он оставался белым листом.

Возможно, объяснение в том, что «…расширение, ARB_compatibility, было представлено, когда был представлен OpenGL 3.1. Наличие этого расширения является сигналом для пользователя о том, что устаревшие или удаленные функции все еще доступны через исходные точки входа и перечисления.» Источник.

Вопрос, а зачем мне вообще старые функции. Ну, новые возможности я ещё досконально не изучил 😇. Во-вторых, хотелось изучить вопрос совместимости. Возможна ли она в принципе? Возможна.

Если серьезно, то вот ситуация. У меня есть просмотр 3D-модели. Меня на этом этапе всё устраивает, кроме ступенек. Я просто хочу включить нормальное сглаживание, не переписывая ничего.

Включил, доволен, дальше буду изучать шейдеры.

Использование

Инициализация и завершение сеанса OpenGL теперь выглядят следующим образом:

Отобразить 3D-модель

Первая часть из постановки задачи — научиться сглаживать, решена. Теперь надо убедиться, что старыми функция можно пользоваться, и что шейдеры для отрисовки заводить не обязательно.

Рисуем сетку земли

Сетка нам нужно, чтобы хоть как-то ориентироваться в пространстве. Рисуем её по старинке, через glBegin..glEnd. То есть метод рисования очень-очень старый. Не стал менять.

Рисуем координатные оси

Можно нарисовать линиями, но хочется иметь масштабируемую толщину и симпатичную объёмную стрелку на конце. Поэтому используем квадрики и метод рисования снова очень старый.

Без сглаживания всё выглядит очень плохо.

Со сглаживанием тот же самый код выглядит очень хорошо.

Рисуем 3D-модель

Для отрисовки модели используем массивы координат вершин, векторов нормалей в вершинах и массив цвета в вершинах.

Код очень тривиален. Подробнейшее описание функций можно найти в каждом первом мануале по OpenGL. Так что тут комментировать нечего. Самое главное тут — правильно сформировать массивы для отрисовки. Массивы, которые сформировались в модели при прочтении файла — непригодны.

Итак, у нас описаны такие массивы:

После того, как мы прочитали модель из файла, запускаем процедуру формирования массивов:

При таком методе отображения, когда один массив индексов работает одновременно на три (а позже и четыре) массива, мы не можем использовать массивы, которые сформировались в модели во время чтения файла. Индекс вершины и индекс нормали к этой вершине в модели не совпадают. Поэтому после прочтения происходит постобработка и очистка массивов модели.

Метод RecreateColors формирует массив цветов в вершинах аналогичным образом. Можно посмотреть в исходниках. Просто он пересоздаётся при изменении параметров выделения части модели, и если публиковать его листинг, то пришлось бы публиковать и всё остальное, а это уже много.

Скрины без сглаживания и со сглаживанием представлены в начале статьи, там где крик души про ступеньки и смысл жизни. Но скрины тут всё таки будут, но по другому поводу.

Казалось бы, линии сетки что со сглаживанием, что без сглаживания, визуально не отличаются. Не, отличаются.

Если посмотрим на модель снизу, или с любого другого ракурса, где линии идут поверх модели, то увидим, что они не сглажены ни разу.

Зато со сглаживанием всё очень хорошо с любого ракурса.

Итог

Конечно, это не самое лучшее сглаживание в мире. Есть продвинутые вещи, которые надо изучать. Но теперь вид модели вызывает положительные эмоции и стимул, как минимум, подключить текстуры и задействовать шейдеры. У меня был опыт подключать текстуры, но это было в мезозое и давно устарело. А шейдерами не занимался никогда, но всегда хотел изучить эту тему.

Поэтому, очень надеюсь, продолжение будет. Просто для этого нужен отрезок времени на несколько дней, когда можно будет безболезненно выключить телегу и телефон.


Скачать

Друзья, спасибо за внимание!

Исходник (zip) 233 Кб. Delphi XE 7

Исполняемый файл (zip) 1.34 Мб (Скомпилирован в XE 7) — дополнительно внутри небольшая коллекция маленьких моделей.

Модельки можно совершенно бесплатно и без регистрации скачать тут: rigmodels.com или creazilla.com.

Исполняемый файл лучше запускать непосредственно из архива. Или распаковывать всё в каталог. Так он по крайней мере будет видеть каталог с моделями.

Управление

Левая кнопка мыши — вращаем камеру

Правая кнопка мыши — отдаляем/приближаем камеру

Нажатое Колесо — перемещаем модель


5 2 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Не нашли ответ на свой вопрос? Задайте его здесь!...x