Урок №29. Сглаживание в OpenGL

  Дмитрий Бушуев  | 

  Обновл. 7 Окт 2021  | 

 8102

На этом уроке мы рассмотрим тему сглаживания в OpenGL.

Алиасинг

В нашем авантюрном путешествии по OpenGL-рендерингу вы, вероятно, натыкались на странные зубчатые узоры у краев используемых в сцене моделей. Причина появления описываемых зазубренных ребер связана с тем, как растеризатор преобразует вершинные данные в реальные фрагменты. Давайте рассмотрим самый обычный куб:

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

Данный эффект носит название алиасинг (англ. «aliasing»). Существует довольно много различных методов сглаживания (англ. «anti-aliasing»), которые призваны бороться с вышеописанным эффектом, создавая более гладкие края.

Первоначально программисты для реализации сглаживания в своих программах использовали метод сглаживания SSAA (от англ. «Super Sample Anti-Aliasing»), который для рендеринга сцены временно использует рендербуфер с гораздо более высоким разрешением. Затем, когда вся сцена будет визуализирована, разрешение уменьшается до нормального. Дополнительное разрешение было использовано для предотвращения появления эффекта зазубренных краев. Но в то же время, как данный метод действительно помогал решить проблему алиасинга, он обладал серьезным недостатком в виде просадки по производительности, так как нам приходится рисовать намного больше фрагментов, чем обычно. Поэтому данная техника имела лишь короткий момент славы.

Дальнейшее развитие метода SSAA породило более современную технику сглаживания MSAA (от англ. «MultiSample Anti-Aliasing»), которая заимствует концепции SSAA, реализуя гораздо более эффективный подход в сглаживании зазубренных краев объекта. На этом уроке мы подробно обсудим метод MSAA, являющийся встроенной технологией сглаживания в OpenGL.

Мультисэмплинг (MSAA)


Чтобы понять, что такое мультисэмплинг и каков его механизм решения проблемы сглаживания, нам сначала нужно немного углубиться во внутреннюю работу растеризатора в OpenGL.

Растеризатор — это комбинация всех алгоритмов и процессов, находящихся между финальным результатом обработки вершин и фрагментным шейдером. Растеризатор берет все вершины, принадлежащие заданному примитиву, и преобразует их в набор фрагментов. Координаты вершин теоретически могут иметь любое значение, а вот фрагменты — нет, так как они связаны разрешением вашего экрана. Почти никогда не бывает однозначного отождествления между координатами вершин и фрагментами, поэтому растеризатор должен каким-то образом определить, в какой фрагмент/экранную координату попадет каждая конкретная вершина.

На этом рисунке мы видим сетку пикселей экрана, где в центре каждого пикселя находится точка выборки (или «сэмпл»), которая используется для определения того, покрыт ли пиксель треугольником. Красные точки выборки являются покрытыми точками, и для этого покрытого пикселя будет сгенерирован фрагмент. Несмотря на то, что некоторые части ребер треугольника все еще проходят через определенные пиксели экрана, точка выборки подобного пикселя не является покрытой внутренней частью треугольника, поэтому этот пиксель не будет подвержен влиянию какого-либо фрагментного шейдера.

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

Из-за ограниченного количества пикселей экрана некоторые пиксели будут визуализироваться вдоль ребра, а некоторые — нет. В результате, мы визуализируем примитивы с несглаженными краями, что приводит к появлению у объектов сцены зазубренных ребер, которые мы видели раньше.

Алгоритм работы мультисэмплинга заключается в том, что для определения покрытия треугольником он использует не одну точку выборки, а несколько (отсюда и происходит его название). Вместо одной центральной точки выборки мы поместим в пиксель 4 точки подвыборки и будем использовать их для определения покрытия пикселей.

В левой части изображения показано, как мы обычно определяем покрытие треугольника. Конкретно этот пиксель не будет запускать фрагментный шейдер (и, следовательно, останется пустым), так как его точка выборки не была покрыта треугольником. В правой части изображения показана версия с мультисэмплингом, где каждый пиксель содержит 4 точки выборки. Здесь мы видим, что треугольник покрывает только 2 точки.

Примечание: Количество точек выборки может быть любое, чем больше точек — тем лучше точность покрытия.

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

На самом деле, MSAA работает следующим образом: фрагментный шейдер запускается только один раз на пиксель (для каждого примитива) независимо от того, сколько подвыборок покрывает треугольник; фрагментный шейдер работает с вершинными данными, интерполированными к центру пикселя. Затем MSAA использует буфер глубины/трафарета, чтобы определить покрытие подвыборки. Количество охваченных подвыборок определяет то, насколько цвет пикселя вносит свой вклад в соответствующий цвет во фреймбуфере. Поскольку на предыдущем изображении были покрыты только 2 выборки из 4, то цвета треугольника наполовину смешиваются с цветом фреймбуфера (в данном случае, с цветом фона), что приводит к светло-голубому цвету.

В результате получается буфер с более высоким разрешением (с более высокими разрешениями глубины/трафарета), где все ребра заданного примитива будут иметь более гладкие края. Давайте посмотрим, как выглядит мультисэмплинг, когда мы определяем покрытие рассмотренного ранее треугольника:

Как вы можете заметить, каждый пиксель содержит 4 подвыборки (нерелевантные выборки были скрыты): синие подвыборки покрыты треугольником, а серые — нет. Пиксели, находящиеся во внутренней области треугольника, запустят фрагментный шейдер один раз, при этом их выходной цвет будет сохранен непосредственно во фреймбуфере (при условии отсутствия смешивания). Однако на границе треугольника не все подвыборки будут им покрыты, поэтому результат выполнения фрагментного шейдера будет использован фреймбуфером лишь частично. В зависимости от количества покрытых треугольником выборок, цвет границы треугольника будет в большей или меньшей степени определяться цветом соответствующего пикселя.

Для каждого пикселя — чем меньше подвыборок является частью треугольника, тем в меньшей степени они соответствуют цвету треугольника. Если бы мы раскрасили предыдущую картинку в фактические цвета пикселей, то получили бы что-то вроде этого:

Как видно, кромка треугольника теперь окружена более светлыми цветами. В результате чего край, если смотреть на него издалека, будет выглядеть сглаженным.

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

Вышеописанные действия дают нам общее представление об устройстве MSAA-сглаживания. Фактическая же логика, лежащая в основе растеризатора, работает немного сложнее, но данного краткого описания должно быть достаточно, чтобы понять концепцию и принципы работы MSAA-сглаживания и углубиться в практические аспекты его реализации.

MSAA-сглаживание в OpenGL

Если мы хотим использовать MSAA, то нам нужно выбрать такой буфер, который будет способен хранить более одного значения цвета выборки на пиксель. Нам потребуется новый тип буфера, с возможностью хранить заданное количество подвыборок, и он называется — мультисэмпл-буфер.

Многие оконные системы вместо стандартного буфера способны предоставить нам мультисэмпл-буфер. GLFW также содержит подобный функционал, и всё что нам нужно сделать — это намекнуть GLFW, что мы хотели бы использовать не обычный буфер, а мультисэмпл-буфер с N точками выборки. Для этого, перед созданием окна, вызываем функцию glfwWindowHint():

Теперь, при вызове функции glfwCreateWindow(), создается окно рендеринга, но на этот раз с буфером, содержащим 4 подвыборки на координату экрана. Это означает, что размер буфера увеличивается в 4 раза.

Теперь, когда мы попросили GLFW задействовать мультисэмпл-буферы, нужно включить режим мультисэмплинга, воспользовавшись вызовом функции glEnable() с параметром GL_MULTISAMPLE. В большинстве OpenGL-драйверов мультисэмплинг включен по умолчанию, поэтому на первый взгляд явный вызов glEnable() может показаться немного избыточным, но лучше включить его в любом случае:

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

Ребра куба действительно выглядят намного более гладкими, и то же самое будет происходить с любым другим объектом, который вы отрисуете в своей сцене.

  GitHub / Урок №29. Сглаживание в OpenGL — Исходный код №1

Внеэкранный MSAA


Благодаря тому, что вся работа по созданию мультисэмплированных буферов ложится на плечи GLFW, включение режима MSAA-сглаживания является довольно простой задачей. Однако, если мы хотим использовать наши собственные фреймбуферы, мы должны сгенерировать мультисэмплированные буферы самостоятельно.

Существует два способа создания мультисэмплированных буферов для использования их в качестве прикрепляемых объектов фреймбуферов:

   в виде прикрепляемого объекта текстуры;

   в виде прикрепляемого объекта рендербуфера.

Мультисэмплированная текстура в качестве прикрепляемого объекта

Чтобы создать текстуру, поддерживающую хранение нескольких точек выборки, вместо функции glTexImage2D() мы будем использовать функцию glTexImage2DMultisample(), которая принимает аргумент GL_TEXTURE_2D_MULTISAMPLE:

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

Чтобы прикрепить мультисэмплированную текстуру к фреймбуферу, мы используем функцию glFramebufferTexture2D(), но на этот раз с параметром GL_TEXTURE_2D_MULTISAMPLE:

Теперь текущий связанный фреймбуфер имеет мультисэмплированный цветовой буфер в виде текстурного изображения.

Мультисэмплированный рендербуфер

Как и в варианте с текстурами, так же просто создать и объект мультисэмплированного рендербуфера. Всё, что нам нужно сделать — это изменить вызов функции glRenderbufferStorage() на вызов функции glRenderbufferStorageMultisample() при конфигурировании памяти рендербуфера (связанного в данный момент):

Единственное, что изменилось — это дополнительный второй параметр, где мы устанавливаем количество выборок, которые хотели бы использовать; здесь мы используем 4.

Рендеринг в мультисэмплированный фреймбуфер

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

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

Функция glBlitFramebuffer() переносит заданную область источника, определяемую 4 координатами экранного пространства, в заданную целевую область, также определенную 4 координатами экранного пространства. Возможно, вы помните из урока о фреймбуферах в OpenGL, что если мы привязываемся к GL_FRAMEBUFFER, то мы привязываемся как к объектам чтения, так и к объектам рисования фреймбуфера. Мы также могли бы привязаться к этим объектам индивидуально, привязав фреймбуферы к GL_READ_FRAMEBUFFER и GL_DRAW_FRAMEBUFFER соответственно. Функция glBlitFramebuffer() считывает данные из этих двух целевых объектов, чтобы определить, какой из них является источником, а какой — целевым фреймбуфером. Затем мы могли бы перенести вывод мультисэмплированного фреймбуфера на фактический экран, блиттируя изображение в заданный по умолчанию фреймбуфер, например:

Если бы мы затем визуализировали наше приложение, то получили бы тот же результат: зеленый куб, отображаемый с использованием MSAA-сглаживания и со значительно менее зазубренными краями (ребрами):

Но что делать, если бы мы захотели использовать текстуру мультисэмплированного фреймбуфера для выполнения таких вещей, как постобработка? Мы не можем напрямую использовать мультисэмплированные текстуры во фрагментном шейдере. Однако то, что мы можем сделать — это блиттировать мультисэмплированный буфер(ы) в другой фреймбуфер с прикрепленным объектом, не являющимся мультисэмплированной текстурой. Затем, для постобработки, мы задействуем прикрепляемый объект обычной цветной текстуры, успешно производя постобработку изображения, визуализируемого с помощью мультисэмплинга. Это означает, что мы должны сгенерировать новый фреймбуфер, который действует исключительно как промежуточный объект фреймбуфера, в который будет преобразован мультисэмплированный буфер; обычная 2D-текстура, которую мы можем использовать во фрагментном шейдере. Данный процесс можно представить следующим образом:

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

  GitHub / Урок №29. Сглаживание в OpenGL — Исходный код №2

Примечание: Поскольку текстура экрана снова является нормальной (не мультисэмплированной) текстурой, то некоторые фильтры постобработки, такие как edge-detection, снова привносят неровные края. Чтобы приспособиться к этому, вы можете впоследствии размыть текстуру или создать свой собственный алгоритм сглаживания.

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

Собственные алгоритмы сглаживания

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

Чтобы получить значение текстуры для каждой подвыборки, необходимо использовать uniform-сэмплер типа sampler2DMS, вместо обычного sampler2D:

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

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


Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (17 оценок, среднее: 5,00 из 5)
Загрузка...

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *