Урок №39. SSAO в OpenGL

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

  Обновл. 8 Окт 2020  | 

 442

На этом уроке мы рассмотрим, что такое SSAO в OpenGL и как его использовать.

SSAO

Фоновое освещение — это фиксированная световая константа, добавляемая к общему освещению сцены для имитации рассеивания света. В реальном мире свет рассеивается во всех направлениях и с различной интенсивностью, поэтому опосредованно освещенные части сцены также должны иметь различную интенсивность освещения. Одним из видов аппроксимации непрямого освещения является фоновое затенение (сокр. «AO» от англ. «Ambient Occlusion»), которое пытается имитировать непрямое освещение путем затемнения складок, отверстий и поверхностей, расположенных близко друг к другу. Эти области в значительной степени перекрыты окружающей геометрией, из-за чего у лучей света становится меньше шансов выбраться наружу, а поэтому подобные области кажутся темнее. Взгляните на углы и складки вашей комнаты — вы увидите, что свет в них кажется немного темнее.

Ниже приведен пример изображения сцены с использованием метода фонового затенения и без него. Обратите внимание, что между складками и в углах (фоновое) освещение выглядит более темным:

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

Алгоритмы расчета AO являются довольно ресурсоёмкими, поскольку они должны учитывать окружающую геометрию. На первый взгляд кажется, что для каждой точки в пространстве можно было бы испускать большое количество лучей, чтобы определить величину её фонового затенения, но этот процесс очень быстро упрется в ограничения производительности для решений, работающих в реальном времени. В 2007 году компания Crytek в своей игре под названием Crysis использовала методику, известную как SSAO (от англ. «Screen-Space Ambient Occlusion»). Вместо реальных геометрических данных для определения величины фонового затенения, вышеупомянутый метод задействует буфер глубины сцены в координатах экранного пространства. По сравнению с обычным AO рассматриваемый подход является невероятно быстрым в вычислительном плане и дает правдоподобные результаты, что делает его де-факто стандартом для моделирования фонового затенения в реальном времени.

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

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

Становится понятно, что качество и точность эффекта напрямую связаны с количеством точек выборок, которые мы используем. Если их слишком мало, то точность метода резко снижается, и мы получаем артефакт в виде некоторой полосатости/сегментации изображения, называемый бандингом (от англ. «banding»); если же точек выборок слишком много, то падает производительность алгоритма. Мы можем уменьшить требование к используемому количеству выборок, введя некоторую случайность в ядро выборки. Произвольно вращая ядро выборки каждого фрагмента, мы можем получить высококачественные результаты с гораздо меньшим количеством непосредственно самих выборок. Однако данная случайная составляющая вносит заметную картину шума, которую нам придется исправить, применив к полученным результатам размытие. Ниже приведено изображение (любезно предоставленное John Chapman), демонстрирующее эффект бандинга и влияние случайной составляющей на конечный результат:

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

Метод SSAO, разработанный компанией Crytek, имел определенный узнаваемый визуальный стиль. Поскольку формой использованного ядра выборки была сфера, то плоские стенки выглядели серыми, поскольку половина точек выборки из ядра в конечном итоге оказывалась внутри окружающей геометрии. Ниже представлено SSAO-изображение Crysis в режиме «градации серого», на котором наглядно изображен описываемый эффект:

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

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

Буферы выборки


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

   вектор позиции каждого фрагмента;

   вектор нормали каждого фрагмента;

   цвет альбедо каждого фрагмента;

   ядро выборки;

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

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

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

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

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

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

Примечание: Векторы позиции можно воссоздать по одним значениям глубины, используя некоторые хитрые трюки, которые описал Matt Pettineo в своем блоге. Это потребует нескольких дополнительных вычислений в шейдерах, но избавит нас от необходимости хранить данные о позициях в G-буфере (тем самым экономя довольно много памяти). Чтобы не усложнять пример, мы оставим рассмотрение данных оптимизаций за рамками данного урока.

Текстура позиций gPosition настраивается следующим образом:

Тем самым мы имеем текстуру позиций (координат), которую можем использовать для получения значений глубины каждой точки выборки из ядра. Обратите внимание, что мы храним координаты в формате данных типа с плавающей точкой; в нашем случае, значения координат не привязываются к диапазону [0.0, 1.0]. Также обратите внимание на метод наложения текстур GL_CLAMP_TO_EDGE. Его использование гарантирует, что мы не произведем случайно выборки значения положения/глубины на экранном пространстве из точки за пределами текстуры. Далее нам нужно создать само полусферическое ядро выборки и реализовать некоторый метод его вращения в случайном направлении.

Нормально-ориентированная полусфера

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

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

Мы случайным образом меняем значения координат x и y в касательном пространстве в диапазоне [-1.0, 1.0], а также координату z в диапазоне [0.0, 1.0] (если бы мы меняли z в диапазоне [-1.0, 1.0], то у нас было бы сферическое ядро выборки, а не полусферическое). Поскольку ядро выборки будет ориентировано вдоль нормали к поверхности, то все результирующие векторы точек выборки окажутся лежащими внутри полусферы.

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

Где функция lerp() определена как:

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

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

Случайные повороты ядра


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

Мы создадим массив 4×4 векторов случайного поворота, ориентированных вдоль вектора нормали поверхности касательного пространства:

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

Затем мы создаем текстуру 4×4, которая содержит векторы случайного поворота; убедитесь, что режим её наложения задан как GL_REPEAT:

Теперь у нас есть все необходимые исходные данные, чтобы перейти к реализации SSAO.

SSAO-шейдер

SSAO-шейдер работает на экранном 2D-прямоугольнике и вычисляет значение затенения для каждого из его фрагментов. Поскольку нам нужно сохранить результат этапа SSAO (для использования в финальном шейдере освещения), то мы создаем еще один объект фреймбуфера:

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

Полный процесс рендеринга SSAO будет выглядеть примерно так:

Шейдер shaderSSAO принимает в качестве входных данных соответствующие текстуры G-буфера, текстуру шума и точки выборки из ядра нормально-ориентированной полусферы:

Интересно отметить переменную noiseScale. Мы хотим наложить текстуру шума на весь экран, но, поскольку TexCoords варьируется от 0.0 до 1.0, текстура texNoise не будет наложена соответствующим образом. Поэтому рассчитаем необходимую величину для масштабирования TexCoords, разделив размеры экрана на размер текстуры шума:

В связи с тем, что мы устанавливаем параметры наложения текстуры texNoise в GL_REPEAT, то она будет повторяться по всему экрану. Используя переменную fragPos и вектор нормали normal, мы можем создать TBN-матрицу, которая преобразует любой вектор из касательного пространства в пространство вида:

Используя процесс ортогонализации Грамма-Шмидта, мы создаем ортогональный базис, каждый раз слегка отклоненный в зависимости от значения randomVec. Обратите внимание, поскольку для построения касательного вектора мы задействуем случайный вектор, то нет необходимости в том, чтобы TBN-матрица была точно выровнена вдоль поверхности геометрии, а поэтому и нет необходимости в касательных (и бикасательных) векторах для каждой вершины.

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

Здесь kernelSize и radius — это переменные, которые мы можем использовать для настройки эффекта; в данном случае их значения равны 64 и 0.5 соответственно. Для каждой итерации мы сначала преобразуем вектор соответствующей точки выборки в пространство вида. Далее к координатам фрагмента в пространстве вида мы добавляем полученное значение смещения точки выборки в пространстве вида. Затем умножаем значение смещения на radius, чтобы увеличить (или уменьшить) эффективный радиус выборки SSAO.

После этого необходимо преобразовать вектор sample в экранное пространство, чтобы мы могли использовать значение позиции/глубины переменной sample, как если бы мы отображали её положение непосредственно на экране. Поскольку вектор в данный момент находится в пространстве вида, то мы сначала должны преобразовать его в отсеченное пространство, используя uniform-переменную матрицы проекции projection:

После того, как переменная преобразуется в отсеченное пространство, мы выполняем шаг деления перспективы (делим компоненты x-, y-, z- на w-компоненту). Затем полученные нормализованные координаты устройства преобразуются в диапазон [0.0, 1.0], чтобы мы могли использовать их для выборки значений из текстуры позиций:

Мы используем компоненты x- и y- вектора offset для выборки значений из текстуры позиций, чтобы получить глубину (или значение z-компоненты) точки выборки, наблюдаемой с позиции зрителя (первый незатененный видимый фрагмент). Затем мы проверяем, больше ли текущее значение глубины выборки, чем сохраненное значение глубины, и если — да, то добавляем к окончательному коэффициенту затенения:

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

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

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

Здесь мы использовали GLSL-функцию smoothstep(), которая плавно интерполирует свой третий параметр между диапазоном первого и второго параметров, возвращая 0.0, если он меньше или равен первому параметру, и 1.0, если он равен или больше второго параметра. Если разница глубин лежит в пределах radius, то её значение плавно интерполируется от 0.0 до 1.0 по следующей кривой:

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

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

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

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

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

Размытие фонового затенения


Между проходом SSAO и этапом прохода освещения, в первую очередь необходимо размыть SSAO-текстуру. Итак, давайте создадим еще один объект фреймбуфера для хранения результата размытия:

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

Мы перемещаемся по окружающим текселям текстуры SSAO в диапазоне от -2.0 до 2.0, производя выборки из текстуры SSAO в количестве, идентичном размерам текстуры шума. Смещаем каждую координату текстуры на размер одного текселя, используя функцию textureSize(), которая возвращает размер в виде переменной типа vec2. Далее усредняем полученные результаты для создания простого, но эффективного размытия:

Иии… вот оно: текстура с информацией о фоновом затенении каждого фрагмента, готовая к использованию в проходе освещения.

Применение фонового затенения

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

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

  Google Drive / Исходный код — Урок №39. SSAO в OpenGL

  GitHub / Исходный код — Урок №39. SSAO в OpenGL

Заключение


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

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

Поиграйте с различными сценами и различными параметрами, чтобы оценить возможности кастомизации SSAO-эффекта.

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

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

   SSAO Tutorial: отличный туториал по SSAO от John Chapman; большая часть кода и методов данного урока основана на его статье.

   Know your SSAO artifacts: отличная статья об улучшении специфических артефактов SSAO.

   SSAO With Depth Reconstruction: отличное добавление к данному уроку по SSAO от OGLDev по вопросу восстановления векторов позиций по значениям глубины.

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

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

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

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