SSLR: Screen Space Local Reflections в AAA-играх

image Привет, друг! В этот раз я опять подниму вопрос о графике в ААА-играх. Я уже разобрал методику HDRR (не путать с HDRI) тут и чуть-чуть поговорил о коррекции цвета. Сегодня я расскажу, что такое SSLR (так же известная как SSPR, SSR): Screen Space Local Reflections. Кому интересно — под кат.Введение в Deferred Rendering Для начала введу такое понятие как Deferred Rendering (не путать с Deferred Shading, т.к. последнее относится к освещению). В чем суть Deferred Rendering? Дело в том, что все эффекты (такие как освещение, глобальное затенение, отражения, DOF) можно отделить от геометрии и реализовать эти эффекты как особый вид постпроцессинга. К примеру, что нужно, чтобы применить DOF (Depth Of Field, размытие на дальних расстояниях) к нашей сцене? Иметь саму сцену (Color Map) и иметь информацию о позиции текселя (другими словами на сколько пиксель далеко от камеры). Далее — все просто. Применяем Blur к Color Map, где радиус размытия будет зависеть от глубины пикселя (из Depth Map). И если взглянуть на результат — чем дальше объект, тем сильнее он будет размыт. Так что же делает методика Deferred Rendering? Она строит так называемый GBuffer, который, обычно, в себя включает три текстуры (RenderTarget): Color map (информация о диффузной составляющий или просто цвет пикселя)image Normal map (информация о нормали «пикселя»)image Depth map (информация о позиции «пикселя», тут храним только глубину)image В случае с Color map, Normal map вроде все понятно, это обычные Surface.Color текстуры: пожалуй, за исключением того, что вектор нормали может лежать в пределах [-1, 1] (используется простая упаковка вектора в формат [0, 1]).

А вот ситуация с Depth map становится непонятной. Как же Depth map хранит в себе информацию о позиции пикселя, да еще и одним числом? Если говорить сильно упрощенно, трансформация примитива:

float4 vertexWVP = mul (vertex, World*View*Projection); Дает нам экранные координаты:

float2 UV = vertexWVP.xy; И некоторую информацию о том, насколько «далеко» от камеры пиксель:

float depth = vertexWVP.z / vertexWVP.w; Исходя из этого UV нам не нужен, т.к. при рисовании обычного квада на весь экран он и так известен. Поэтому стоит хранить в карте глубины не позицию пикселя, а только глубину.

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

float3 GetPosition (float2 UV, float depth) { float4 position = 1.0f; position.x = UV.x * 2.0f — 1.0f; position.y = -(UV.y * 2.0f — 1.0f);

position.z = depth; //Transform Position from Homogenous Space to World Space position = mul (position, InverseViewProjection); position /= position.w;

return position.xyz; } Напомню, что для построения GBuffer необходима такая методика как MRT (Multiple Render Targets), которая рисует модель сразу в несколько Render Target (причем в каждом RT содержится разная информация). Одно из правил MRT — размерность всех Render Target должна быть одинаковой. В случае Color Map, Normal Map — Surface.Color: 32-ух битная RT, где на каждый канал ARGB приходится по 8 бит, т.е. 256 градаций от 0 до 1.

Благодаря такому подходу мы можем применять сложные эффекты к любой геометрии, например самый популярный Screen Space эффект: SSAO (Screen Space Ambient Occlusion). Этот алгоритм анализирует буферы глубины и нормали, считая уровень затенения. Весь алгоритм я описывать не буду, он уже описывался на хабре, скажу лишь то, что задача алгоритма сводится к трассировки карты глубины: у нас есть набор случайных векторов, направленных из считаемого «пикселя» и нам нужно найти кол-во пересечений с геометрией.

Пример эффекта (слева без SSAO, справа с SSAO):

image Так же Deferred Shading является Screen Space эффектом. Т.е. для каждого источника света на экране (без всяких оптимизаций) мы рисуем квад в режиме Additive в так называемый RenderTarget: Light Map. И зная мировую позицию «пикселя», его нормаль, позицию источника света — мы можем посчитать освещенность этого пикселя.

Пример Deferred Shading (освещение выполнено отложено, после отрисовки геометрии):

image Достоинства и проблемы Screen Space эффектов Самый главный плюс Screen Space эффектов — независимость сложности эффекта от геометрии.Самый главный минус — локальность всех эффектов. Дело в том, что мы постоянно будем сталкиваться с Information Lost, во многих случаях это сильно зависит обзора, поскольку SSE зависит от смежных глубин текселей, которые могут быть сгенерированы любой геометрией.

Ну и стоит отменить, что Screen Space эффекты выполняются полностью на GPU и являются пост-процессингом.

Наконец SSLR После всей теории мы подошли к такому эффекту, как Screen Space Local Reflections: локальные отражения в экранном пространстве.Для начала разберемся с перспективной проекцией:

image Горизонтальный и вертикальный угол зрения задается FOV (обычно 45 градусов, я предпочитаю 60 градусов), в виртуальной камере они разные т.к. учитывается еще и Aspect Ratio (соотношение сторон).

Окно проекции (там, где мы оперируем UV-space данными) — это, что мы видим, на то мы проецируем нашу сцену.Передняя и задняя плоскости отсечения это соответственно Near Plane, Far Plane, задаются так же в проекцию как параметры. Делать в случае Deferred Rendering слишком большим значением Far Plane стоит, т.к. точность Depth Buffer сильно упадет: все зависит от сцены.

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

float3 GetPosition (float2 UV, float depth) { float4 position = 1.0f; position.x = UV.x * 2.0f — 1.0f; position.y = -(UV.y * 2.0f — 1.0f);

position.z = depth; position = mul (position, InverseViewProjection); position /= position.w;

return position.xyz; } После нам нужно найти вектор взгляда на этот пиксель:

float3 viewDir = normalize (texelPosition — CameraPosition); В качестве CameraPosition выступает позиция камеры.И найти отражение этого вектора от нормали в текущем пикселе: float3 reflectDir = normalize (reflect (viewDir, texelNormal)); Далее задача сводится к трассировке карты глубины. Т.е. нам нужно найти пересечение отраженного вектора с какой-либо геометрией. Понятное дело, что любая трассировка производится через итерации. И мы в них сильно ограниченны. Т.к. каждая выборка из Depth Map стоит времени. В моем варианте мы берем некоторое начальное приближение L и динамически меняем его исходя из расстояния между нашим текселем и позицией, которую мы «восстановили»: float3 currentRay = 0;

float3 nuv = 0; float L = LFactor;

for (int i = 0; i < 10; i++) { currentRay = texelPosition + reflectDir * L;

nuv = GetUV (currentRay); // проецирование позиции на экран float n = GetDepth (nuv.xy); // чтение глубины из DepthMap по UV

float3 newPosition = GetPosition2(nuv.xy, n); L = length (texelPosition — newPosition); } Вспомогательные функции, перевод мировой точки на экранное пространство:

float3 GetUV (float3 position) { float4 pVP = mul (float4(position, 1.0f), ViewProjection); pVP.xy = float2(0.5f, 0.5f) + float2(0.5f, -0.5f) * pVP.xy / pVP.w; return float3(pVP.xy, pVP.z / pVP.w); } После завершения итераций мы имеет позицию «пересечения с отраженной геометрией». А наше значение nuv будет проекцией этого пересечения на экран, т.е. nuv.xy — это UV координаты в экранном нашем пространстве, а nuv.z это восстановленная глубина (т.е. abs (GetDepth (nuv.xy)-nuv.z) должен быть очень маленьким).

В конце итераций L будет показывать расстояние отраженного пикселя. Последний этап — собственно добавление отражения к Color Map:

float3 cnuv = GetColor (nuv.xy).rgb; return float4(cnuv, 1); Разбавим теорию иллюстрациями, исходное изображение (содержание Color Map из GBuffer):

c5caaa03385f4090a139d72688f9733b.png После компиляции шейдера (отражения) мы получим следующую картину (Color Map из GBuffer + результат шейдера SSLR):

1708cc7c107d4b9db774594537bf7a74.png Не густо. И тут стоит еще раз напомнить, что Space-Screen эффекты это сплошной Information Lost (примеры выделены в красные рамки).

Дело в том, что если вектор отражения выходит за пределы Space-Screen — информация о Color-карте становится недоступной и мы видим Clamping нашего UV.

Чтобы частично исправить эту проблему, можно ввести дополнительный коэффициент, который будет отражать «дальность» отражения. И далее по этому коэффициенту мы будем затенять отражение, проблема частично решается:

L = saturate (L * LDelmiter);

float error *= (1 — L); Результат, отражение умноженное на error (попытка убрать артефакт SSLR — information lost):

4b6bdfbf34944e328ce53207a28b8f99.png Уже лучше, но мы замечаем еще одну проблему, что будет, если вектор отразится в направлении камеры? Clamping«а UV происходить не будет, однако, несмотря на актуальность UV (x > 0, y > 0, x < 1, y < 1) он будет неверным:

28ce703be6a943e38fc8ba6b1925b9da.png Эту проблему так же можно частично решить, если как-нибудь ограничить углы допустимых отражений. Для этого идеально подходит фишка с углами от эффекта Френеля:

float fresnel = dot (viewDir, texelNormal); Чуть-чуть модифицируем формулу: float fresnel = 0.0 + 2.8 * pow (1+dot (viewDir, texelNormal), 2); Значения Френеля, с учетом Normal-маппинга (значения fresnel-переменной для SSLR-алгоритма): 003ce4e5a3644be1bd9429d9c3ef9cf7.png Те области, которые отражаются в «камеру» будут черными, и их мы не учитываем (взамен можно сделать fade в кубическую текстуру).

Отражение, умноженное на error и fresnel (попытка удалить большую часть артефактов SSLR):

1c560e826a7a42eb905fc93d1b8932d5.png Кстати, значение Fresnel стоит лимитировать по какому-либо параметру, т.к. из-за «шероховатости» нормалей значение будет на порядок больше единицы (или другого числа-лимитера).

И завершающий этап сегодняшний статьи — это размытие отражений, т.к. идеальное отражение только у зеркала. Степень размытия можно считать как 1-error (чем дальше отраженный пиксель — тем сильнее размыт). Это будет своеобразный вес размытия и хранить его можно в альфа-канале RT-отражений.

Результат (финальное изображение с убранными артефактами и с размытыми отражениями):

a347aecffc114e46adba1e09e635ebb1.png Заключение Так же, стоит добавить некоторую информацию об отражающей способности: насколько четкое отражение, насколько поверхность вообще способна отражать, в те места где SSLR не работает — добавить статическое отражение кубической текстуры.Конечно, Space-Screen эффекты не являются честными, и разработчики стараются скрыть артефакты, но сейчас в реалтайме подобное (при сложной геометрии) сделать невозможно. А без подобных эффектов игра начинает выглядеть как-то не так. Я описал общую методику SSLR: основные моменты из шейдера я привел. Код, к сожалению, прикрепить не могу, т.к. в проекте слишком много зависимостей.

Удачных разработок! ;)

© Habrahabr.ru