Урок №37. Эффект свечения в OpenGL

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

  Обновл. 27 Дек 2021  | 

 7985

 ǀ   3 

На этом уроке мы рассмотрим создание эффекта свечения в OpenGL.

Эффект свечения

Довольно часто перед программистами встает вопрос о том, как реализовать в сцене ощущение очень яркого источника света или интенсивно освещенной области. Одним из способов выделения источников света на экране монитора является добавление эффекта свечения (англ. «bloom»). Благодаря этому у зрителя создается иллюзия, что источники света (или яркие области) выглядят действительно очень яркими. Пример сцены с эффектом свечения и без него можно увидеть ниже (изображение любезно предоставлено Epic Games):

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

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

Реализация эффекта свечения


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

Давайте проиллюстрируем этот процесс пошагово. Мы визуализируем сцену, заполненную четырьмя яркими источниками света, представляемыми в виде цветных ящиков. Цветные световые ящики имеют значения яркости от 1.5 до 15.0. Если бы мы визуализировали это в цветовой HDR-буфер, то сцена выглядела бы следующим образом:

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

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

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

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

На следующем рисунке кратко представлены необходимые шаги для реализации эффекта свечения:

Первый шаг требует, чтобы мы извлекли все яркие цвета сцены на основе некоторого порогового значения. Давайте этим и займемся.

Извлечение яркого цвета

Как уже было сказано, первый шаг требует, чтобы мы извлекли все яркие цвета сцены. Для этого нам понадобятся два изображения визуализированной сцены. Мы могли бы визуализировать сцену дважды, разными шейдерами в разные фреймбуферы, но лучше использовать аккуратный маленький трюк под названием «MRT» (сокр. от англ. «Multiple Render Targets»), который позволит нам указать более одного выхода фрагментного шейдера; благодаря этому мы получим возможность извлечь два изображения за один проход рендеринга. Используя layout location перед выходными переменными фрагментного шейдера, мы можем управлять тем, в какой цветовой буфер будет писать фрагментный шейдер:

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

Теперь мы должны явно сообщить OpenGL, что собираемся рендерить в несколько цветовых буферов. Для этого воспользуемся функцией glDrawBuffers(). По умолчанию OpenGL рендерит только в первый прикрепленный цветовой буфер фреймбуфера, игнорируя все остальные. Чтобы выполнить рендеринг в оба буфера, необходимо передать массив перечислений прикрепляемых цветовых буферов, которые мы бы хотели визуализировать в последующих операциях:

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

В вышеописанном фрагменте кода мы сначала стандартным образом вычисляем освещение и передаем его в первую выходную переменную FragColor фрагментного шейдера. Затем мы используем то, что в данный момент хранится в FragColor, чтобы определить, превышает ли его яркость определенный порог. Мы вычисляем яркость фрагмента, преобразуя его цвет в оттенки серого соответствующим образом (взяв скалярное произведение обоих векторов — мы эффективно умножаем каждый отдельный компонент обоих векторов и складываем результаты вместе). Если яркость превышает определенный порог, то мы выводим цвет во второй цветовой буфер. То же самое делаем для световых ящиков.

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

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

Теперь изображение извлеченных ярких областей необходимо размыть. Мы могли бы сделать это с помощью простого фильтра из урока по фреймбуферам (подраздел «Постобработка»), но лучше воспользуемся более продвинутым (и более привлекательным) фильтром размытия, называемым «Размытие по Гауссу» (англ. «Gaussian blur»).

Размытие по Гауссу


Размытие по Гауссу основано на Гауссовой кривой, которая обычно описывается как колоколообразная кривая, дающая высокие значения вблизи её центра и постепенно убывающие — к её краям. Гауссова кривая может быть математически представлена в различных формах, но обычно она имеет следующий вид:

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

Для реализации описанного фильтра размытия нам понадобится двумерный массив весовых коэффициентов, который мы можем получить из двумерного уравнения Гауссовой кривой. Однако с данным подходом есть одна проблема — он быстро становится чрезвычайно вычислительно затратным. Возьмем, к примеру, ядро размытия размером 32×32 — это потребует от нас проведения выборки из текстуры в общей сложности 1024 раза для каждого фрагмента!

К счастью для нас, уравнение Гаусса имеет очень изящное свойство, которое позволяет разделить двумерное уравнение на два меньших одномерных уравнения: первое описывает горизонтальные веса, а второе — вертикальные веса. Затем, на текстуре сцены, мы сначала выполняем горизонтальное размытие с горизонтальными весами, а после, на полученной текстуре, выполняем вертикальное размытие. Благодаря этому свойству получаются точно такие же результаты, но при этом мы экономим невероятное количество производительности, т.к. теперь нам придется сделать только 32+32 выборки, по сравнению с прошлым значением в 1024 шт.! Данный метод называется двухпроходным размытием по Гауссу.

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

Прежде чем мы углубимся во фреймбуферы, давайте сначала обсудим фрагментный шейдер размытия по Гауссу:

Мы берем относительно небольшую выборку Гауссовых весовых коэффициентов, каждый из которых используется для задания определенного веса горизонтальным или вертикальным выборкам вокруг текущего фрагмента. Вы можете видеть, что мы разделили фильтр размытия на горизонтальную и вертикальную секции на основе произвольного значения, которое мы установили uniform-переменной horizontal. В качестве базы смещения используется размер текселя, полученный делением значения 1.0 на размер текстуры (vec2-значение, возвращаемое функцией textureSize()).

Для размытия изображения мы создаем два основных фреймбуфера, каждый из которых имеет только текстуру цветового буфера:

Затем, после того как мы получили HDR-текстуру и извлеченную текстуру яркости, мы сначала заполняем один из «пинг-понг фреймбуферов» текстурой яркости, а затем размываем её 10 раз (5 раз по горизонтали и 5 раз по вертикали):

На каждой итерации мы связываем один из двух фреймбуферов, в зависимости от того, хотим ли мы производить горизонтальное или же вертикальное размытие, с цветовым буфером другого фреймбуфера в качестве текстуры, которую нужно размыть. На первой итерации мы специально связываем текстуру, которую хотим размыть (brightnessTexture), так как в противном случае оба цветовых буфера окажутся пустыми. Повторяя этот процесс 10 раз, обработка текстуры яркости заканчивается полным размытием по Гауссу, которое накладывается одно на другое 5 раз. Данное построение позволяет нам размывать любое изображение так часто, как нам бы этого хотелось; чем больше итераций размытия по Гауссу, тем сильнее эффект размытия.

Размывая извлеченную текстуру яркости 5 раз, мы получаем должным образом размытое изображение всех ярких областей сцены:

Последний шаг для завершения построения эффекта свечения — это объединение полученной размытой текстуры яркости с HDR-текстурой исходной сцены.

Смешивание полученных текстур

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

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

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

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

  GitHub / Урок №37. Эффект свечения в OpenGL — Исходный код

Заключение


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

Дополнительные ресурсы

   Efficient Gaussian Blur with linear sampling — очень хорошо описывает размытие по Гауссу и то, как улучшить его производительность с помощью билинейной выборки текстур.

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

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

Комментариев: 3

  1. Моисей:

    Здравствуйте, Юрий! Огромное спасибо за ваши уроки!)
    Когда закончите раздел с OpenGL, было бы здорово, если бы вы стали бы вести уроки по сетевому программированию на С++, это так, просто идея)
    А вообще ваш сайт самый лучший по программированию на С++, это касается, как изложения материала, так и его визуализации!)

    1. Фото аватара Юрий:

      Здравствуйте! Уроки по OpenGL — это, прежде всего, заслуга Дмитрия Бушуева, а не моя (так как именно он является автором/переводочиком). А за сайт спасибо 🙂

      1. Фото аватара Дмитрий Бушуев:

        Юра, спасибо за тёплые слова. 🙂

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

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