что такое затенение ssao
Под капотом: объяснение SSAO
Техника имеет свои ограничения и причуды. В последние годы она использовалась в нескольких играх ААА, и даже если она не идеальна, она помогает системе человеческого восприятия лучше понимать сцену, и мы надеемся, что ее добавление в набор технологий наших симов грузовиков будет полезным. Без сомнения, мы хотим ввести дополнительные способы теневого вычисления, которые улучшат или даже вытеснят его.
Мы находимся под постоянным давлением, чтобы улучшить внешний вид нашей игры с помощью вокальной базы наших фанатов. В то же время всегда есть желание заставить игры работать быстрее. Вдобавок к этим иногда конкурирующим запросам, исходящим от базы игроков, наш собственный художественный отдел всегда стремится заполучить новые графические игрушки, чтобы сделать нашу игру богаче и лучше. Всякий раз, когда мы вводим в игру новую графическую функцию, мы стараемся сделать это так, чтобы это не повлияло на производительность игроков со старыми компьютерами, мы не хотим делать игру несовместимой с нашими существующими клиентами. Вот почему есть возможность полностью отключить SSAO и несколько настроек его качества / производительности.
Работа наших программистов над новыми технологиями SSAO / HBAO также потребовала изменений в нашем конвейере искусства и создания произведений искусства. Все 3D-модели в наших играх должны были быть повторно рассмотрены отделом по художественным работам, и любые случаи, когда любые искусственные тени и затемнения уже применялись к модели художником, были изменены. Для некоторых более сложных игровых моделей это было упрощением, которое фактически уменьшило количество треугольников в них, чтобы повысить производительность рендеринга. В какой-то степени мы обменяли часть предполагаемых будущих ручных усилий, которые потребовались бы для создания отдельных красивых 3D-моделей для алгоритмического прохода рендеринга, который объединяет теневое представление для всей сцены, помогая «укоренять» такие объекты, как здания, фонарные столбы и растительность на местности. Что такое SSAO и как это работает?
Поэтому вместо того, чтобы запекать статическую информацию (которая также занимала бы много времени и места для хранения, учитывая масштаб нашей карты мира), мы хотим вычислять ее на лету, во время выполнения. Таким образом, мы можем рассчитать его также для взаимодействия с транспортными средствами, открытия мостов, анимированных объектов и так далее. Хотя есть одна загвоздка. Для такого вычислительного подхода у нас есть только данные, которые видны на экране (вспомним «экранное пространство»), поэтому, как только какая-то часть игрового мира выходит из видимой рамки, ее нельзя использовать для оценки окклюзии. Это ограничение создает различные артефакты, такие как исчезновение окклюзии на стене, первоначально вызванное объектом, который только что оказался за краем экрана и, таким образом, стал невидимым не только для вас, но и для алгоритма, поэтому он перестал вносить вклад в вычисление окклюзии.
Мы упоминали, что методика нашего выбора основана на горизонте. Это означает, что мы не исследуем окружающую среду, снимая лучи в трехмерном мире, вместо этого мы анализируем полусферу выше / вокруг каждого пикселя, чтобы увидеть, как далеко он открывается, пока не заблокирован, сколько света пропускает окружающая геометрия используя z-буфер в качестве нашего прокси. Полушарие фактически аппроксимируется несколькими прогонами вдоль линии, повернутой вокруг данного пикселя. Если мы сможем полностью следовать вдоль этого полушария, то нет преграды. Если алгоритм выбирает значение в z-буфере, которое блокирует входящий свет, он определяет уровень окклюзии. Алгоритм оптимизирован для производительности, но его ограничение заключается в том, что как только он попадает во что-либо, даже, возможно, в небольшой объект, он прекращает дальнейшее исследование. Это может вызвать «чрезмерную окклюзию» проблема и может быть замечена как визуальный артефакт, когда какой-то относительно тонкий объект, такой как столб дорожного знака, вызывает сильную окклюзию на соседней стене. Вы можете попытаться обнаружить такие маленькие объекты и пропустить их, что, в свою очередь, может привести к «недостаточной окклюзии» на тонких уступах. Мы выбрали первое.
Так что, как видите, идея не такая уж сложная, для опытного программиста в любом случае;), но здесь требуется много вычислений, что создает некоторую нагрузку на 3D-ускоритель. Итак, мы создали несколько профилей производительности, каждый из которых использует сочетание методов оптимизации:
Мы надеемся, что вся эта информация была интересной и полезной для вас. Мы отправим вам виртуальную пятерку, если вы прочитаете эту статью до этого момента. Вы заслуживаете печенье и большую чашку горячего шоколада! Если вы все еще хотите получить более подробную информацию по этой теме, см., Например, эту ссылку [www.cse.chalmers.se].
Спасибо за ваше время и поддержку, и мы еще увидимся в некоторых следующих статьях из раздела «Под капотом», которые мы время от времени приносим для нашего #BestCommunityEver.
Также не забывайте, что летняя распродажа Steam скоро заканчивается! Посетите нашу страницу для разработчиков.
Normal-oriented Hemisphere SSAO для чайников
Привет, хабрапользователь! После небольшого перерыва можно опять браться за трехмерную графику. В этот раз мы поговорим о таком алгоритме глобального затенения, как Normal-oriented Hemisphere SSAO. Интересно? Под кат!
Но сначала чуть-чуть новостей
Я отказался от использования XNA, мощностей DX9 мне стало не хватать: конечно, в целом ничего не поменялось, но написание кода стало куда менее костыльным. Все последующие примеры будут реализованы с помощью фреймворка SharpDX.Toolkit: не пугайтесь, это духовный наследник XNA, еще и OpenSource и с поддержкой DX11.
Классически — теории
Самой важной частью в графическом движке любой игры (которая имеет претензии на реалистичность) — это освещение. Сейчас невозможно полностью смоделировать освещение в игре real-time так, как это происходит в нашем, реальном мире. Условно говоря, не в real-time приложениях: освещение считается “пусканием” фотонов из источника света в нужных направлениях и регистрации этих фотонов камерой (глазом). Для подобных процессов в реальном времени требуется апромиксация, например: у нас есть некоторая поверхность и источник света, и для того что-бы создать освещение – требуется рассчитать “освещенность” каждого пикселя принадлежащей поверхности, т.е. учитывается только прямое влияние источника света на тексель. В данной апромиксации не учитывается непрямое освещение, т.е. в случае с real-time фотон может отразиться от какой-либо поверхности и повлиять на совершено другой “тексель”. Для единичных, небольших источников света это не особо критично, но стоит взять большой источник света и “бесконечно удаленный”, например, солнце (небо выступает как мощный «рассеиватель» света от солнца), то сразу возникают проблемы, примерно такие:
В реальном же мире, на подобной сцене не было бы такой черной черноты в местах теней. Развивая дальше тему, можно ввести некоторое значение ambient, которое будет отображать общую освещенность всей сцены, своеобразная аппроксимация непрямого освещения. Но дело в том, что подобное освещение на всей сцене везде одинаково, даже в тех местах, где непрямой свет будет оказывать наименьшее влияние. Но и тут можно схитрить и усложнить апромиксацию путем затенения тех участков, куда отраженному свету сложнее всего добраться. Таким образом мы подошли к понятию называемым “глобальное затенение” (ambient occlusion). Суть такого подхода заключается в том, что мы для каждого фрагменты сцены находим некоторый заграждающий фактор, т.е. кол-во не загражденных направлений падения “фотона” деленное на общее кол-во всевозможных направлений.
Рассмотрим следующую картинку:
Тут у нас есть две рассматриваемые точки, которые образуют вокруг себя окружность с радиусом R. И для того, чтобы определить степень загражденности взятого фрагмента достаточно найти площадь незагражденного пространства и разделить на общую площадь окружности. Если мы подобную операцию проделаем для всех точек сцены – мы получим глобальное затенение. Выглядеть оно будет примерно так (для трехмерного случая):
Но теперь нужно подумать, как подобный алгоритм внедрить в пайп-лайн рендера графического конвейера. Сложность возникает в том, что отрисовка геометрии происходит постепенно. В следствии чего, первый объект в сцене не будет знать о существовании других. Можно, конечно, заранее рассчитать AO (на этапе загрузки) для сцены, но в таком случае мы не будем учитывать динамически изменяемую геометрию: физические объекты, персонажей, etc. И тут на помощь приходит работа с геометрией в экранном пространстве (Screen Space). Я его уже упоминал, когда рассказывал об SSLR-алгоритме. Этим можно воспользоваться и считать AO в экранном пространстве. Тут появляется самая классическая реализация SSAO, придумали его классные ребята из крайтек ровно 8 лет назад. Их алгоритм заключался в следующем: после рисования всей геометрии у них был в наличии буфер глубины, который несет в себе информацию об всей видимой геометрии, строя сферы для каждого текселя они считали кол-во затенения для сцены:
Тут, кстати, возникает еще одна сложность. Дело в том, что мы не можем учесть абсолютно все направления в real-time, во первых, потому, что пространство дискретно, а во вторых на производительности можно ставить крест. Мы не можем учесть даже 250 направлений (а именно столько необходимо для минимально-вменяемого качества изображения). Для того, чтобы сократить кол-во выборок – используют некоторое ядро направлений (от 8 до 32), которое вращают каждый раз на случайное значение. После этих операций нам доступен AO в реал-тайме:
Самое тяжелое в алгоритме SSAO это определение заграждения, ведь это чтение из float-текстуры.
Чуть позже была придумана модификация алгоритма SSAO: Normal-oriented Hemisphere SSAO. Суть модификации в том, что мы можем увеличить точность алгоритма за счет учета нормалей (по сути нужен GBuffer). Для пространства выборок мы будем использовать не сферу, а полусферу, которая ориентирована по нормали текущего текселя. Такой подход позволяет увеличить кол-во полезных выборок в двое.
Если посмотреть на рисунок, то можно понять, о чем я говорю:
Завершающим этапом алгоритма будет размытие изображения AO для того, чтобы убрать шум, вызванным случайными выборками. В конечном счете – реализация нашего алгоритма будет выглядеть так:
С теорией пока все ясно, можно перейти к практике.
Зона, свободная от теории
Советую прочитать эту статью, там я рассказывал про суть работы Screen Space пространством. Но, а в практике я приведу особо важные участки кода с нужными комментариями.
Самое первое, что нам понадобится, это информация о геометрии: GBuffer. Т.к. его построение не входит в тему статьи – о нем подробно расскажу как-нибудь в другой раз.
Второе — это полусфера со случайными направлениями:
Тут важно отметить, что в шейдере у нас не будет трассировки, т.к. мы сильно ограничены в инструкциях, взамен этому – мы будем считать факт нахождения конечной точки в какой-либо геометрии, поэтому необходимо учитывать больше ближней геометрии, чем дальней. Для этого достаточно взять набор точек с нормальным распределением в полусфере. Это можно получить честным нормальным распределением, можно просто дважды умножить вектор на случайное число от 0 до 1, а можно воспользоваться небольшим хаком: задавать длину какой-либо функцией, например квадратичной. Это нам даст более лучший “сорт” ядра.
Третье – это набор каких-нибудь случайных векторов, для того, чтобы разнообразить конечные выборки, у меня оно генерируется в случайным образом:
Но выглядит оно примерно так:
Не стоит использовать подобную текстуру больше чем 4×4-8×8, потому, что подобное вращение ядра дает низкочастотный шум, который размыть в будущем куда проще.
Теперь поглядим на тело шейдера SSAO:
Тут мы получаем нелинейную глубину, получаем мировую позицию и нормаль, получаем набор случайных векторов растянутых на весь экран. Стоит сразу заранее сказать про два хака.
Первый заключается в том, что мы сдвигаем позицию текселя на нормаль умноженную на некоторое маленькое значение, это необходимо для того, чтобы избавится от ненужных пересечений из-за дискретности screen space пространства:
А второй заключается в том, что в алгоритме нам необходимо сравнивать значения глубины, а нелинейная глубина на средних-дальних дистанциях находится в окрестностях единицы. По-хорошему мы должны эту глубину линеализировать, но т.к. подобные значения используются только для сравнения – можно ввести некоторое оценку нелинейной глубины:
Отдельно стоит сказать, что хорошо бы сделать unroll-цикла, т.к. кол-во выборок заранее известно, подобный код будет работать быстрее.
Дальше начинается сам алгоритм:
Вращаем ядро и ориентируем это ядро по нормали в текстеле:
И передаем функции расчета заграждения:
Берем сэмпл и проектируем его в экранное пространство (получаем новые значения UV.xy и нелинейную глубину):
Функция проекции выглядит следующим образом:
Константы 0.5f напрашиваются, чтобы их зашили в матричку.
После этого мы получаем новое значение глубины:
Факт заграждения мы определяем как: “видна ли точка наблюдателю”, т.е. если точка не лежит в какой-либо геометрии – то assessReaded будет всегда строго меньше assessProjected.
Ну и с учетом того, что в экранном пространстве полно такого явления как information lost, мы должны регулировать кол-во затенения в зависимости от дистанции “проникновения” в геометрию. Это необходимо для того, что мы ничего не знаем о геометрии за видимой частью экранного пространства:
Ну и финальный этап, это размытие. Я лишь скажу то, что нельзя размывать буффер SSAO без учета неоднородности глубины как это делают многие. Так же, хорошо бы учесть и нормали при размытии, примерно так:
Коэффициенты depthFactor и normalFactor учитываются в коэффициентах размытия.
Взамен заключения
Для более подробного изучения – я оставлю полный исходный код тут, а для любителей увидеть своим глазом демо тут.
Кстати, в демо я намерено оставил NORMAL_BIAS равным нулю, чтобы увидеть проблему, кроме того, в GBuffer рисуется только геометрия и нет normal-маппинга, из-за чего на дальних дистанциях происходит z-fighting.
В будущих статьях постараюсь осветить другие алгоритмы real-time ao, такие как HBAO, HDAO, HBAO+, если будет интересен к этой теме, конечно.
Learn OpenGL. Урок 5.10 – Screen Space Ambient Occlusion
Тема фонового освещения была затронута нами в уроке по основам освещения, но лишь вскользь. Напомню: фоновая составляющая освещения – суть постоянная величина, добавляемая во все расчеты освещения сцены для имитации процесса рассеяния света. В реальном же мире свет испытывает множество переотражений с разной степенью интенсивности, что приводит к столь же неравномерной засветке косвенно освещенных участков сцены. Очевидно, что засветка с постоянной интенсивностью не очень правдоподобна.
Одним из видов приближенного расчета затенения от непрямого освещения является алгоритм фонового затенения (ambient occlusion, AO), который имитирует ослабление непрямого освещения в окрестности углов, складок и прочих неровностях поверхностей. Такие элементы, в основном, значительно перекрываются соседствующей геометрией и потому оставляют меньше возможностей лучам света вырваться наружу, затемняя данные участки.
Ниже представлено сравнение рендера без и с использованием алгоритма AO. Обратите внимание на то, как падает интенсивность фонового освещения в окрестности углов стен и прочих резких изломов поверхности:
Стоит отметить, что алгоритмы расчета AO являются довольно ресурсоемкими, поскольку требуют анализа окружающей геометрии. В наивной реализации можно было бы просто в каждой точке поверхности выпустить множество лучей и определить степень её затенения, но такой подход весьма быстро достигает допустимого для интерактивных приложений предела ресурсоемкости. К счастью, в 2007 году компания Crytek опубликовала работу с описанием собственного подхода в реализации алгоритма фонового затенения в экранном пространстве (Screen-Space Ambient Occlusion, SSAO), который использовался в релизной версии игры Crysis. Подход рассчитывал степень затенения в экранном пространстве, используя лишь текущий буфер глубины вместо реальных данных об окружающей геометрии. Такая оптимизация радикально ускорила алгоритм по сравнению с эталонной реализацией и при этом давала по большей части правдоподобные результаты, что сделало данный подход приближенного расчета фонового затенения стандартом де-факто в индустрии.
Принцип, на котором основан алгоритм довольно прост: для каждого фрагмента полноэкранного квада рассчитывается коэффициент затенения (occlusion factor) на основе значений глубины окружающих фрагментов. Вычисленный коэффициент затенение далее используется для уменьшения интенсивности фонового освещения (вплоть до полного исключения). Получение коэффициента требует сбора данных о глубине от множества выборок из сферической области, окружающей рассматриваемый фрагмент, и сравнения этих значений глубины с глубиной рассматриваемого фрагмента. Число выборок, имеющих глубину бОльшую, нежели текущий фрагмент непосредственно и определяют коэффициент затенения. Посмотрите на данную схему:
Здесь, каждая серая точка лежит внутри некоторого геометрического объекта, а потому осуществляет вклад в значение коэффициента затенения. Чем больше выборок окажутся внутри геометрии окружающих объектов, тем меньше будет остаточная интенсивность фонового затенения в этой области.
Очевидно, что качество и реалистичность эффекта прямо зависит от числа сделанных выборок. При малом числе выборок точность алгоритма падает и приводит к появлению артефакта бэндинга (banding) или «полошения» из-за резких переходов между областями с сильно отличающимися коэффициентами затенения. Большое же число выборок просто убивает производительность. Рандомизации ядра выборок позволяет при схожих по качеству результатах несколько снизить число требуемых выборок. Подразумевается переориентация поворотом на случайный угол набора векторов выборок. Однако внесение случайности тут же приносит новую проблему в виде заметного шумового узора, что требует использования фильтров размытия для сглаживания результата. Ниже приведен пример работы алгоритма (автор – John Chapman) и его типичные проблемы: бэндинг и шумовой узор.
Как видно, заметное полошение из-за малого числа выборок неплохо убирается внесением рандомизации ориентации выборок.
Конкретная реализация SSAO от Crytek обладала узнаваемым визуальным стилем. Поскольку специалисты Crytek использовали сферическое ядро выборки это сказывалось даже на плоских поверхностях типа стен, делая их затененными – ведь половина объема ядра выборки оказывалась погруженной под геометрию. Ниже – скриншот со сценой из Crysis, изображенной в градациях серого на основе значения коэффициента затенения. Здесь хорошо виден эффект «серости»:
Для избегания такого влияния мы перейдем от сферического ядра выборки к полусфере, ориентированной вдоль нормали к поверхности:
Осуществляя выборку из такой полусферы ориентированной по нормали (normal-oriented hemisphere) нам не придется учитывать в расчете коэффициента затенения фрагменты, лежащие под поверхностью прилегающей поверхности. Такой подход убирает излишнее затенение в, в целом, дает более реалистичные результаты. Данном урок будет использовать подход с полусферой и немного доработанный код из блестящего урока по SSAO от John Chapman.
Буфер с исходными данными
Процесс вычисления коэффициента затенения в каждом фрагменте требует наличия данных об окружающей геометрии. Конкретно, нам потребуются следующие данные:
Поскольку SSAO является эффектом, реализующимся в экранном пространстве, то непосредственный расчет возможно выполнить отрендерив полноэкранный квад. Но тогда у нас не будет данных о геометрии сцены. Чтобы обойти такое ограничение, мы осуществим рендер всей необходимой информации в текстуры, которые позже будут использованы в шейдере SSAO для доступа к геометрической и прочей информации о сцене. Если вы внимательно следовали данным урокам, то уже должны узнать в описанном подходе облик алгоритма отложенного затенения. Во многом поэтому эффект SSAO как родной встает в рендер с отложенным затенением – ведь текстуры, хранящие координаты и нормали, уже доступны в G-буфере.
В данном уроке эффект реализуется поверх несколько упрощенной версии кода из урока об отложенном освещении. Если вы еще не ознакомились с принципами отложенного освещения – настоятельно советую обратиться к этому уроку.
Поскольку доступ к пофрагментной информации о координатах и нормалях уже должен быть доступен за счет G-буфера, то фрагментный шейдер стадии обработки геометрии достаточно прост:
Поскольку алгоритм SSAO является эффектом в экранном пространстве, а коэффициент затенения вычисляется на основе видимой области сцены, то есть смысл вести расчеты в видовом пространстве. В данном случае переменная FragPos, полученная из вершинного шейдера, хранит положение именно в видовом пространстве. Стоит удостовериться, что данные о координатах и нормалях хранятся в G-буфере в видовом пространстве, поскольку все дальнейшие расчеты будут осуществляться в нем же.
Существует возможность восстановления вектора положения на основе лишь известной глубины фрагмента и некоторого количества математической магии, что описано, например, у Matt Pettineo в блоге. Это, конечно, требующий бОльших затрат на расчеты способ, однако он избавляет от необходимости хранить данные о положении в G-буфере, что занимает уйму видеопамяти. Однако, ради простоты кода примера, мы оставим этот подход для личного изучения.
Текстура буфер цвета gPosition сконфигурирована следующим образом:
Данная текстура хранит координаты фрагментов и может быть использована для получения данных о глубине для каждой точки из ядра выборок. Отмечу, что текстура использует формат данных с плавающей точкой – это позволит координатам фрагментов не быть приведенными к интервалу [0., 1.]. Также обратите внимание на режим повтора – установлен GL_CLAMP_TO_EDGE. Это необходимо для устранения возможности не нарочно осуществить оверсэмплинг в экранном пространстве. Выход за пределы основного интервала текстурных координат даст нам некорректные данные о положении и глубине.
Далее займемся формированием полусферического ядра выборок и созданием метода случайной его ориентации.
Создание ориентированной по нормали полусферы
Итак, стоит задача создать набор точек выборки, расположенных внутри полусферы, сориентированной вдоль нормали к поверхности. Поскольку создание ядра выборки для всех возможных направлений нормали вычислительно недостижимо, то мы используем переход в касательное пространство, где нормаль всегда представляется как вектор в направлении положительной полуоси Z.
Предполагая радиус полусферы единичным процесс формирования ядра выборки из 64 точек выглядит так:
Здесь мы случайным образом выбираем координаты x и y в интервале [-1., 1.], а координату z – в интервале [0., 1.] (будь интервал таким же, как для x и y, мы бы получили сферическое ядро выборки). Результирующие вектора выборок окажутся ограничены полусферы, поскольку ядро выборки в конечном итоге будет сориентировано вдоль нормали к поверхности.
В данный момент все точки выборки случайно распределены внутри ядра, но в угоду качеству эффекта выборкам, лежащим ближе к началу координат ядра, стоило бы вносить больший вклад в расчете коэффициента затенения. Это можно реализовать за счет изменения распределения сформированных точек выборки, увеличив их плотность около начала координат. Такую задачу легко выполнить с помощью функции интерполяции с ускорением:
Функция lerp() определена как:
Такой трюк дает нам модифицированное распределение, где большинство точек выборки лежат вблизи начала координат ядра.
Каждый из полученных векторов выборки будет использован для смещения координаты фрагмента в видовом пространстве для получения данных об окружающей геометрии. Для получения приличных результатов при работе в видовом пространстве может потребоваться внушительное количество отсчетов, что неизбежно ударит по производительности. Однако, внесение псевдослучайного шума или поворота векторов выборок в каждом обрабатываемом фрагменте, позволит значительно снизить требуемое число выборок при сравнимом качестве.
Случайный поворот ядра выборки
Итак, внесение случайности в распределение точек ядра выборки позволяет значительно снизить требование к числу этих точек для получения достойного качества эффекта. Можно было бы создать случайный вектор поворота для каждого фрагмента сцены, но это слишком затратно по памяти. Эффективней создать небольшую текстуру, содержащую набор случайных векторов поворота, а затем просто использовать её с установленным режимом повтора GL_REPEAT.
Создадим массив 4х4 и заполним случайными векторами поворота, сориентированными вдоль вектора нормали в касательном пространстве:
Поскольку ядро выровнено вдоль положительной полуоси Z в касательном пространстве, то компонент z оставляем равным нулю – это обеспечит поворот только вокруг оси Z.
Далее создадим текстуру размером также 4х4 и зальем туда наш массив векторов поворота. Обязательно используйте режим повтора GL_REPEAT для тайлинга текстуры:
Что ж, теперь у нас готовы все данные, необходимые для непосредственной реализации алгоритма SSAO!
Шейдер SSAO
Шейдер эффекта будет исполняться для каждого фрагмента полноэкранного квада, вычисляя коэффициент затенения в каждом из них. Поскольку результаты будут использованы в еще одной стадии рендера, создающей итоговое освещение, нам потребуется создание еще одного объекта фреймбуфера для хранения результата работы шейдера:
Поскольку результат работы алгоритма – единственное вещественное число в пределах [0., 1.], то для хранения будет достаточно создать текстуру с единственной доступной компонентой. Именно поэтому в качестве внутреннего формата для буфера цвета ставится GL_RED.
В целом процесс рендера стадии SSAO выглядит примерно следующим образом:
Шейдер shaderSSAO принимает нужные ему текстуры G-буфера как входные данные, а также шумовую текстуру и ядро выборки:
Обратите внимание на переменную noiseScale. Наша маленькая текстура с шумом должна быть затайлена по всей поверхности экрана, но поскольку текстурные координаты TexCoords заключены в пределах [0., 1.] этого не произойдет без нашего вмешательства. В этих целях мы вычисляем множитель для текстурных координат, который находится как отношение размера экрана к размеру шумовой текстуры:
Поскольку при создании шумовой текстуры texNoise мы установили режим повтора в GL_REPEAT, то теперь она будет повторяться множество раз на поверхности экрана. Имея на руках величины randomVec, fragPos и normal мы можем создать матрицу TBN трансформации из касательного пространства в видовое:
Используя процесс Грамма-Шмидта мы создаем ортогональный базис, случайно наклоненным в каждом фрагменте на основе случайного значения randomVec. Важный момент: поскольку в данном случае нам неважно, чтобы матрица TBN была точно сориентирована вдоль поверхности треугольника (как в случае с parallax mapping’ом, прим. пер.), то нам не нужны предрасчитанные данные о касательных и бикасательных.
Далее мы проходим по массиву ядра выборки, переводим каждый вектор выборки из касательного пространства в видовое и получаем его сумму с текущим положением фрагмента. Затем сравниваем величину глубины получившейся суммы со значением глубины, полученной выборкой из соответствующей текстуры G-буфера.
Пока звучит запутанно, разберем это по шагам:
Здесь kernelSize и radius являются переменными, контролирующими характеристики эффекта. В данном случае они равны 64 и 0.5 соответственно. На каждой итерации мы переводим вектор ядра выборки в видовое пространство. Далее прибавляем к полученному значению смещения выборки в видовом пространстве значение положения фрагмента в видовом пространстве. При этом значение смещения умножается на переменную radius, которая управляет радиусом ядра выборки эффекта SSAO.
После этих шагов нам следует преобразовать полученный вектор sample в экранное пространство, для того, чтобы мы могли осуществить выборку из текстуры G-буфера, хранящей положения и глубины фрагментов, используя полученное спроецированное значение. Поскольку sample находится в видовом пространстве, нам потребуется матрица проекции projection:
После преобразования в клиповое пространство мы вручную осуществляем перспективное деление простым делением компонент xyz на w компоненту. Полученный вектор в нормализованных координатах устройства (NDC) переводится в интервал значений [0., 1.] дабы его можно было использовать как текстурные координаты:
Используем компоненты xy вектора sample для выборки из текстуры положений G-буфера. Получим значение глубины (z компоненты), соответствующее вектору выборки при взгляде с позиции наблюдателя (это первый не заслоненный видимый фрагмент). Если при этом полученная глубина выборки оказывается больше, чем сохраненная глубина, то мы увеличиваем коэффициент затенения:
Обратите внимание на смещение bias, которое добавляется к исходной глубине фрагмента (в примере установлена в 0.025). Это смещение не всегда является обязательным, но наличие переменной позволяет управлять тем, как выглядит эффект SSAO, а также, в определенных ситуациях, убирает проблемы с рябью в затененных областях.
Но и это еще не все, поскольку такая реализация приводит к заметным артефактам. Он проявляется в тех случаях, когда рассматривается фрагмент, лежащий вблизи края некоторой поверхности. В таких ситуациях алгоритм при сравнении глубин неизбежно захватит и глубины поверхностей, которые могут лежать очень далеко позади рассматриваемой. В этих местах алгоритм ошибочно сильно увеличит степень затенения, что создаст заметные темные ореолы по краям объектов. Лечится артефакт введением дополнительной проверки на расстояние (пример за авторством John Chapman):
Проверка будет ограничивать вклад в коэффициент затенения только для значений глубины, лежащих в пределах радиуса выборки:
Также мы применяем функцию GLSL smoothstep(), которая реализует плавную интерполяцию третьего параметра в пределах между первым и вторым. При этом возвращая 0, если третий параметр меньше или равен первому, либо 1, если третий параметр больше либо равен второму. Если разница глубин оказывается в пределах radius, то её величина будет плавно сглажена в интервале [0., 1.] в соответствии с данной кривой:
Если бы мы использовали четкие границы в условиях проверки глубины, то это добавило бы артефакты в виде резких границ в тех местах, где значения разницы глубин оказываются вне пределов radius.
Последним штрихом мы нормализуем величину коэффициента затенения, используя размер ядра выборки и записываем результат. Также мы инвертируем итоговое значение, вычитая его из единицы, дабы можно было использовать конечное значение напрямую для модуляции фоновой составляющей освещения без дополнительных действий:
Для сцены с лежащим знакомым нам нанокостюмом, выполнение SSAO шейдера дает следующую текстуру:
Как видно, эффект фонового затенения создает неплохую иллюзию глубины. Одно только выходное изображение шейдера уже позволяет различить детали костюма и убедиться, что он действительно лежит на полу, а не левитирует на некотором расстоянии от него.
И все же эффект далек от идеала, поскольку шумовой узор, привнесенный текстурой случайных векторов поворота, легко заметен. Для сглаживания результата расчета SSAO мы применим фильтр размытия.
Размытие фонового затенения
После построения результата SSAO и перед финальным сведением освещения необходимо провести размытие текстуры, хранящей данные о коэффициенте затенения. Для этого мы заведем еще один фреймбуфер:
Тайлинг шумовой текстуры в экранном пространстве обеспечивает вполне определенные характеристики случайности, которые можно использовать в свою пользу при создании фильтра размытия:
Итого у нас на руках есть текстура с данными фонового затенения для каждого фрагмента на экране – все готово для стадии финального сведения изображения!
Применение фонового затенения
Этап применения коэффициента затенения в итоговом расчете освещения на удивление прост: для каждого фрагмента достаточно просто умножить значение фоновой составляющей источника света на коэффициент затенения из подготовленной текстуры. Можно взять готовый шейдер с моделью Блинна-Фонга из урока по отложенному затенению и немного его подправить:
Серьезных изменений здесь всего два: переход к расчетам в видовом пространстве и умножение компоненты фонового освещения на значение AmbientOcclusion. Пример сцены с единственным синим точечным источником света:
Полный исходный код лежит здесь.
Проявление эффекта SSAO сильно зависит от параметров типа kernelSize, radius и bias, зачастую их тонкая подстройка – само собой разумеющееся занятие художника при проработке той или иной локации/сцены. Нет каких-то «лучших» и универсальных сочетаний параметров: для одних сцен хорош малый радиус ядра выборки, другие выигрывают от увеличенного радиуса и числа выборок. В примере используется 64 точки выборки, что, откровенно говоря, избыточно, но вы всегда можете отредактировать код и посмотреть, что получится при меньшем числе выборок.
Кроме перечисленных юниформов, которые отвечают за настройку эффекта, существует возможность явно регулировать выраженность эффекта фонового затенения. Для этого достаточно возвести коэффициент в степень, контролируемую еще одним юниформом:
Советую потратить некоторое время на игру с настройками, поскольку это даст лучшее понимание о характере изменений в итоговой картинке.
Подводя итог, стоит сказать, что хотя визуальный эффект от применения SSAO и достаточно слабозаметный, но в сценах с хорошо расставленным освещением он неоспоримо добавляет заметную толику реализма. Иметь такой инструмент в своем арсенале безусловно ценно.