Сглаживание
В своих изысканиях, посвященных трехмерному рендеру вы наверняка сталкивались с появлением пикселизованных зазубрин по краям отрисовываемых моделей. Эти отметины неизбежно появляются из-за принципа преобразования вершинных данных в экранные фрагменты растеризатором где-то в глубине пайплайна OpenGL. К примеру, даже на такой простой фигуре как куб уже заметны эти артефакты:
Беглый взгляд, возможно, и не заметит ничего, но стоит посмотреть внимательней и на гранях куба проявятся означенные зазубрины. Попробуем увеличить изображение:
Нет, это никуда не годится. Разве такое качество изображения хочется видеть в релизной версии своего приложения?
- OpenGL
- Создание окна
- Hello Window
- Hello Triangle
- Shaders
- Текстуры
- Трансформации
- Системы координат
- Камера
Часть 2. Базовое освещение
Часть 3. Загрузка 3D-моделей
Часть 4. Продвинутые возможности OpenGL
- Тест глубины
- Тест трафарета
- Смешивание цветов
- Отсечение граней
- Кадровый буфер
- Кубические карты
- Продвинутая работа с данными
- Продвинутый GLSL
- Геометрический шейдер
- Инстансинг
- Сглаживание
Эффект явной видимости попиксельной структуры изображения по краям объектов называется алиясингом. В индустрии компьютерной графики уже накоплено немало техник, называющихся техниками анти-алиасинга или сглаживания, которые как раз и борются с данным эффектом, позволяя обеспечить плавные переходы на границах объектов.
Так, например, одной из первых была техника суперсэмплинга (super sampling anti-aliasing, SSAA). Реализация выполняется в два прохода: сначала рендер идет во внеэкранный кадровый буфер с разрешением заметно больше экранного; затем изображение переносилось с уменьшением в экранный буфер кадра. Эта избыточность данных за счет разницы в разрешении использовалась для уменьшения эффекта алиасинга и работал метод прекрасно, но было одно «Но»: производительность. Вывод сцены в огромном разрешении отнимал порядочно сил у GPU и век славы этой технологии был недолог.
Но из пепла старой технологии родилась новая, более продвинутая: мультисэмплинг (multi sampling anti-aliasing, MSAA). Основывается она на идеях SSAA, но реализует их гораздо более эффективным методом. В данном уроке мы подробно рассмотрим подход MSAA, который доступен нативно в OpenGL.
Мультисэмплинг
Чтобы понять суть мультисэмплинга и как он работает, нам сперва придётся поглубже залезть в потроха OpenGL и взглянуть на работу её растеризатора.
Растеризатором называют комплекс алгоритмов и процедур, которые стоят между окончательно обработанными вершинными данными и фрагментным шейдером. Растеризатор получает на вход все вершины, относящиеся к примитиву и преобразует эти данные в набор фрагментов. Вершинные координаты, теоретически, могут быть абсолютно любыми, но не координаты фрагментов – они жестко ограничены разрешением вашего устройства вывода и размерами окна. И практически никогда координаты вершины примитива не будут накладываться на фрагменты один к одному: так или иначе растеризатору придется некоторым образом решать в каком фрагменте и по какой экранной координате окажется каждая из вершин.
На изображении показана сетка представляющая экранные пиксели. В центре каждого из них помещена точка выборки/сэмплинга, которая используется для определения того, накрывает ли треугольник данный пиксель. Красным отмечены точки выборки, покрытые треугольником – для них растеризатор сгенерирует соответствующий фрагмент. Несмотря на то, что грани треугольника местами перекрывают некоторые пиксели, они не перекрывают точку сэмплинга – здесь фрагмент не будет создан и фрагментный шейдер для данного пикселя выполняться не будет.
Думаю, вы уже догадались о причинах алиасинга. Рендер данного треугольника на экране будет выглядеть так:
Из-за конечности количества пикселей на экране некоторые, расположенные вдоль граней треугольника, будут закрашены, а некоторые – нет. В результате выходит, что рендерятся примитивы вовсе не с гладкими гранями, что и проявляется в виде тех самых зазубрин.
При использовании мультисэмплинга используется не одна точка для определения перекрытия пикселя треугольником, а несколько (отсюда и название). Вместо одной точки выборки в центре пикселя для определения перекрытия будут использованы 4 точки подвыборки, расположенные по некоторому шаблону. Следствием является то, что размер буфера цвета должен также увеличиться в четыре раза (по количеству используемых точек подвыборки).
Слева показан стандартный подход определения перекрытия. Для выбранного пикселя фрагментный шейдер не будет выполнен, и он останется незакрашенным, поскольку перекрытие не было зарегистрировано. Справа показан случай с мультисэмплингом, где каждый пиксель содержит 4 точки подвыборки. Здесь видно, что треугольник перекрывает только 2 точки подвыборки.
Количество точек подвыборки может быть изменено в определенных пределах. Большее число точек – лучше качество сглаживания.
С этого момента все происходящее становится интереснее. Определив, что две точки подвыборки пикселя были покрыты треугольником, необходимо вывести итоговый цвет для этого пикселя. Первой догадкой было бы выполнить фрагментный шейдер для каждой перекрытой треугольником точки подвыборки и затем усреднить цвета всей точек подвыборки в пикселе. В этом случае пришлось бы несколько раз выполнить фрагментный шейдер с вершинными данными интерполированными к координатам каждой из перекрытых точек подвыборки (дважды в данном примере) и сохранить полученные цвета в этих точках. К счастью, на самом деле процесс мультисэмплинга работает не так – иначе нам пришлось бы выполнять немалое число дополнительных вызовов фрагментного шейдера, что сильно ударило бы по производительности.
По факту при использовании MSAA фрагментный шейдер выполняется ровно один раз вне зависимости от числа закрытых примитивом точек подвыборки. Фрагментный шейдер выполняется с вершинными данными интерполированными к центру пикселя, а полученный при его выполнении цвет сохраняется в каждой из перерытых примитивом точек подвыборки. Когда все точки подвыборки буфера кадра заполнены цветами отрисованных нами примитивов, тогда происходит попиксельное усреднение цветов к одному значению на пиксель. В данном примере только две точки подвыборки были перекрыты и, соответственно, заполнены цветом треугольника. Оставшиеся две были заполнены прозрачным цветом фона. При смешивании цветов этих подвыборок получился светло-синий цвет.
Буфер кадра в результате содержит изображение примитивов с гораздо более сглаженными краями. Посмотрите, как выглядит определение покрытия подвыборок на уже знакомом треугольнике:
Видно, что каждый пиксель содержит по четыре точки подвыборки (не важные для примера пиксели оставлены незаполненными), при этом синим отмечены покрытые треугольником точки подвыборки, а серым – непокрытые. Внутри периметра треугольника для всех пикселей будет единожды вызван фрагментный шейдер, результат выполнения которого будет сохранен во всех четырех подвыборках. На гранях же не все подвыборки будут покрыты, так что результат выполнения фрагментного шейдера сохранен будет лишь в части из них. В зависимости от количества покрытых треугольником точек подвыборки итоговый цвет пикселя определяется на основе цвета самого треугольника и других, сохраненных в точках подвыборки, цветов.
Проще говоря, чем больше подвыборок покрыто треугольником, тем в большей степени цвет пикселя будет соответствовать цвету треугольника. Если сейчас заполнить цвета пикселей также, как в примере с треугольником без использования мультисэмплинга, то выйдет следующая картинка:
Как видно, чем меньше подвыборок пикселя принадлежат треугольнику, тем меньше его цвет соответствует цвету треугольника. Четкие границы треугольника теперь окружены пикселями слегка более светлого оттенка, что создает эффект сглаживания, если наблюдать с расстояния.
Но не только значения цвета подвержены работе алгоритма мультисэмплинга: буфер глубины и трафарета также начинают использовать множественные подвыборки для пикселя. Значение глубины вершины интерполируется для каждой из точек подвыборки перед выполнением теста глубины. Значения трафарета хранятся не для всего пикселя, а для каждой из точек подвыборки. Для нас это означает и рост объема памяти, занимаемой этими буферами, в соответствии с количеством используемых подвыборок.
Здесь мы рассмотрели самые основы того, как функционирует мультисэмплинг. Истинная внутренняя логика работы растеризатора будет посложнее, чем приведенный здесь обзор. Однако, для целей общего понимания принципа и хода работы мультисэмплинга этого вполне достаточно.
Мультисемплинг в OpenGL
Для использования мультисэмплинга в OpenGL необходимо использовать буфер цвета, способный хранить больше, чем одно значение цвета на пиксель (ведь MSAA подразумевает хранение значения цвета в точках подвыборки). Таким образом, нам требуется какой-то особенный тип буфера, который сможет хранить заданное количество подвыборок – мультисэмпл буфер.
Большинство оконных систем могут предоставить нам мультисэмпл буфер вместо стандартного буфера цвета. GLFW также обладает данной функциональностью, все что потребуется – установить специальный флаг, сигнализирующий о нашем желании использовать буфер с N точками подвыборки, вместо стандартного:
glfwWindowHint(GLFW_SAMPLES, 4);
Теперь вызов glfwCreateWindow создаст окно вывода с буфером цвета, хранящим по четыре подвыборки на каждую экранную координату. Также GLFW автоматически создаст буферы глубины и трафарета, использующие те же четыре точки подвыборки на пиксель. И размер каждого из упомянутых буферов вырастет в четыре раза.
После создания мультисэмпл буферов силами GLWL остается включить режим мультисэмплинга уже в OpenGL:
glEnable(GL_MULTISAMPLE);
В большинстве драйверов OpenGL мультисэмплинг активен по умолчанию, поэтому данный вызов будет излишним, но явное включение нужных вам функций – хороший тон, а также это позволит активировать режим вне зависимости от умолчаний конкретной реализации.
Собственно, после заказа мультисэмпл буфера и включения режима вся наша работа закончена, поскольку все остальное ложится на механизмы растеризатора OpenGL и происходит без нашего участия. Если сейчас попробовать вывести зеленый куб, знакомый по самому началу урока, то мы увидим, что его грани теперь гораздо более гладкие:
Действительно, края этого кубика выглядят гораздо более привлекательно. И тот же эффект скажется на любом объекте в вашей сцене.
Исходник примера находится здесь.
Внеэкранный мультисемплинг
Создание основного кадрового буфера с включенным MSAA простая задача, благодаря GLFW. Если же мы хотим создать собственный буфер, например, для внеэкранного рендера, то придется взять этот процесс в свои руки.
Есть два основных метода создания буферов с мультисэмплингом для дальнейшего присоединения к кадровому буферу, сходных с уже разобранными примерами из соответствующего урока: текстурные прикрепления и прикрепления типа рендербуфера.
Текстурное прикрепление с мультисемплингом
Для создания текстуры, поддерживающей множественные подвыборки, используется тип текстурной цели GL_TEXTURE_2D_MULTISAPLE и функция glTexImage2DMultisample вместо привычной glTexImage2D:
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, tex);
glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, GL_RGB, width, height, GL_TRUE);
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);
Второй аргумент указывает число подвыборок в создаваемой текстуре. Если последний аргумент установлен в GL_TRUE, то текстура будет использовать одинаковое число и положение точек подвыборки для каждого текселя.
Для присоединения такой текстуры к объекту кадрового буфера используется все тот же вызов glFramebufferTexture2D, но, в этот раз, с указанным типом текстуры GL_TEXTURE_2D_MULTISAMPLE:
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, tex, 0);
В итоге текущий кадровый буфер будет обеспечен буфером цвета на основе текстуры с поддержкой мультисэмплинга.
Рендербуфер с мультисемплингом
Создание рендербуфера с множеством точек подвыборки не сложнее создание такой текстуры. Более того, оно даже проще: все что нужно сделать, так это сменить вызов glRenderbufferStorage на glRenderbufferStorageMultisample при подготовке памяти под привязанный в данный момент объект рендербуфера:
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, width, height);
Из нового здесь только один дополнительный параметр, идущий после типа цели привязки рендербуфера, который указывает число точек подвыборки. Здесь мы указали четыре таких точки.
Рендер в кадровый буфер с мультисемплингом
Рендер в мультисэмпл кадровый буфер идет автоматически, без требуемых действий с нашей стороны. Каждый раз, когда мы рендерим в привязанный кадровый буфер, растеризатор сам проводит необходимые операции. А мы получаем на выходе буфер цвета (глубины, трафарета) с множеством точек подвыборки. Поскольку кадровый буфер с множеством точек подвыборки все же несколько отличается от обычного, не получится напрямую использовать отдельные его буферы для различный операций, типа текстурной выборки в шейдере.
Изображение с поддержкой мультисэмплинга содержит больше информации, чем обычное, потому необходимо разрешить (resolve) это изображение, или, иными словами, преобразовать его разрешение к меньшему. Эта операция по обыкновению проводится с помощью вызова glBlitFramebuffer, что позволяет скопировать область одного кадрового буфера в другой с попутным разрешением присутствующих буферов с множеством точек подвыборки.
Данная функция осуществляет перенос области-источника, заданной четырьмя координатами в экранном пространстве, в область-приемник, также заданную четырьмя экранными координатами. Напомню урок по кадровым буферам: если мы привязываем объект буфера кадра к цели GL_FRAMEBUFFER, то неявно привязка осуществляется и к цели чтения из буфера кадра и к цели записи в буфер кадра. Для привязки к этим целям по отдельности использутся специальные идентификаторы целей: GL_READ_FRAMEBUFFER и GL_DRAW_FRAMEBUFFER соответственно.
Во время своей работы функция glBlitFramebuffer использует эти точки привязки для определения, какой из буферов кадра является источником изображения, а какой – приемником. В итоге, мы могли бы просто перенести изображение из мультисэмпл буфера кадра в стандартный используя блиттинг:
glBindFramebuffer(GL_READ_FRAMEBUFFER, multisampledFBO);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
Собрав и запустив приложение мы бы получили изображение идентичное предыдущему примеру, не задействовавшему буфер кадра: кислотно-зеленый куб, отрисованный с использованием MSAA, в чем можно убедиться, рассмотрев его грани – они все так же гладки:
Исходники примера находятся здесь.
Но что делать в случае, если изображение из буфера кадра с множеством точек подвыборки мы бы хотели использовать в качестве источника данных для постпроцессинга? Напрямую использовать мультисэмпл текстуры в шейдере мы не можем. Но можно попробовать перенести изображение из мультисэмпл буфера кадра с помощью блиттинга в другой, с обычными, не мультисэмпл, буферами. А далее можно использовать обычное изображение как ресурс для постобработки, по сути получая все преимущества MSAA и добавляя сверху этого постобработку. Да, для всего этого процесса придется заводить отдельный буфер кадра, служащий чисто как вспомогательный объект для разрешения MSAA текстур в обычные, которые можно использовать в шейдере. В виде псевдокода процесс выглядит так:
unsigned int msFBO = CreateFBOWithMultiSampledAttachments();
// затем создайте еще один FBO с обычной текстурой в качестве прикрепления цвета
...
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, screenTexture, 0);
...
while(!glfwWindowShouldClose(window))
{
...
glBindFramebuffer(msFBO);
ClearFrameBuffer();
DrawScene();
// разрешение мультисэмпл буфера с помощью вспомогательного
glBindFramebuffer(GL_READ_FRAMEBUFFER, msFBO);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, intermediateFBO);
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
// теперь образ сцены сохранен в обычной текстуре, которая используется для постобработки
glBindFramebuffer(GL_FRAMEBUFFER, 0);
ClearFramebuffer();
glBindTexture(GL_TEXTURE_2D, screenTexture);
DrawPostProcessingQuad();
...
}
Если мы добавим этот код к примерам постобработки из урока по буферу кадра, то сможем применить все те эффекты к изображению сцены без зазубренных краев. Например, с эффектом размытия получится что-то подобное:
Поскольку для постпроцессинга используется стандартная текстура с одной точкой подвыборки, то некоторые методы обработки (поиск границ, например) могут снова внести в сцену заметные резкие края и зазубрины. Чтобы обойти этот артефакт придётся либо размывать результат, либо реализовать свой алгоритм сглаживания.
Как видно, для сочетания MSAA и техник внеэкранного рендера приходится учесть некоторые детали. Но все дополнительные усилия окупаются гораздо более высоким качеством результирующего изображения. Однако, помните, что активация мультисэмплинга все же может значительно затронуть итоговую производительность, особенно при установленном большом количестве точек подвыборки.
Собственный метод сглаживания
На самом деле в шейдеры можно передать мультисэмпл текстуры напрямую, без блиттинга в вспомогательную обычную. В таком случае возможности GLSL предоставляют доступ к отдельным точкам подвыборки в текстуре, что может использоваться в создании собственных алгоритмов сглаживания (что нередко в крупных графических приложениях).
Для начала потребуется создание специального сэмплера типа sampler2DMS, вместо привычного sampler2D:
uniform sampler2DMS screenTextureMS;
А для получения значения цвета в точке подвыборки используется следующая функция:
vec4 colorSample = texelFetch(screenTextureMS, TexCoords, 3); // считывание из 4ой точки подвыборки
Здесь видно дополнительный аргумент – номер точки подвыборки (отсчет с нуля), к которой происходит обращение.
Подробности создания специальных алгоритмов сглаживания здесь рассматривать мы не будем – это не более чем точка отсчета для ваших собственных исследований по этой теме.
P.S.: У нас есть телеграм-конфа для координации переводов. Если есть серьезное желание помогать с переводом, то милости просим!
Автор: UberSchlag