пятница, 27 апреля 2012 г.

SSAO - Простой и практический подход.

Глобальное освещение (GI) — термин, используемый в компьютерной графике для вычисления освещения, вызванного неким взаимодействием между ближайшими поверхностями. Довольно часто термин GI используют только при окрашивании поверхности обьектов лучом отраженным от обьектов окружающей среды. Прямое освещение непосредственно от источника света – легко вычисляется в режиме реального времени на современном оборудовании, но мы не можем сказать то же самое о GI потому, что нам нужно обработать информацию о ближайших поверхностях для каждой точки в сцене, а управлять этим процессом еще довольно сложно. Однако есть определенные методы вычислений GI, которыми управлять не так сложно. Когда свет падает на сцену,или отражается от поверхности, то в сцене могут иметь место некоторые точки, которые имеют меньше шансов получить порцию света: уголки, трещиты между объектами, складки, и др. Это приводит к появлению тех областей, которые в результате будут темнее, чем окружающие их обьекты.

Предпосылки

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

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


Алгоритм

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

Дистанция "d" до окклюдера
Угол между нормалью "N" и векторм между occluder и occludee "V".

С учетом этих двух факторов выводится простая формула для вычисления затенения:

 Occlusion = max( 0.0, dot( N, V) ) * ( 1.0 / ( 1.0 + d ) )

Первое выражение max( 0.0, dot( N,V ) ), основывается на интуитивном предположении, что точки над вершиной вносят больший вклад в затенение, чем точки находящиеся левее или правее. Второе выражение  ( 1.0 / ( 1.0 + d ) )  смягчает  линейность эффекта зафисимую от расстояния. Можно использовать квадратичное затухания или воспользоваться любой другой функцией, это просто дело вкуса. Алгоритм очень прост: выбираем несколько соседей вокруг текущего пиксела и накапливаем их вклад в затенение с использованием формулы выше.

Чтобы выбрать окклюдеры, я использую 4 выборки (<1,0>, <-1,0>, <0,1>, <0,-1>) которые повернуты на 45 ° и 90 ° и отражаются в последствии случайным образом используя текстуру случайных нормалей.

Это код шейдеров HLSL для эффекта, который должен применяться к полноэкранному квадрату:

sampler g_buffer_norm; 
sampler g_buffer_pos; 
sampler g_random; 
float random_size; 
float g_sample_rad; 
float g_intensity; 
float g_scale; 
float g_bias; 
 
struct PS_INPUT 
     float2 uv : TEXCOORD0; 
}; 
 
struct PS_OUTPUT 
     float4 color : COLOR0; 
}; 
 
float3 getPosition(in float2 uv) 
    return tex2D(g_buffer_pos,uv).xyz; 
 
float3 getNormal(in float2 uv) 
    return normalize(tex2D(g_buffer_norm, uv).xyz * 2.0f - 1.0f); 
 
float2 getRandom(in float2 uv) 
     return normalize(tex2D(g_random, g_screen_size * uv / random_size).xy * 2.0f - 1.0f); 
 
float doAmbientOcclusion(in float2 tcoord,in float2 uv, in float3 p, in float3 cnorm) 
   float3 diff = getPosition(tcoord + uv) - p; 
   const float3 v = normalize(diff); 
   const float d = length(diff)*g_scale; 
   return max(0.0,dot(cnorm,v)-g_bias)*(1.0/(1.0+d))*g_intensity; 
 
PS_OUTPUT main(PS_INPUT i) 
   PS_OUTPUT o = (PS_OUTPUT)0; 
 
   o.color.rgb = 1.0f; 
   const float2 vec[4] = {float2(1,0),float2(-1,0), 
                                     float2(0,1),float2(0,-1)}; 
 
float3 p = getPosition(i.uv); 
float3 n = getNormal(i.uv); 
float2 rand = getRandom(i.uv); 
 
float ao = 0.0f; 
float rad = g_sample_rad/p.z; 
 
//**SSAO Calculation**// 
int iterations = 4; 
for (int j = 0; j < iterations; ++j) 
  float2 coord1 = reflect(vec[j],rand)*rad; 
  float2 coord2 = float2(coord1.x*0.707 - coord1.y*0.707, 
                          coord1.x*0.707 + coord1.y*0.707); 
   
  ao += doAmbientOcclusion(i.uv,coord1*0.25, p, n); 
  ao += doAmbientOcclusion(i.uv,coord2*0.5, p, n); 
  ao += doAmbientOcclusion(i.uv,coord1*0.75, p, n); 
  ao += doAmbientOcclusion(i.uv,coord2, p, n); 
ao/=(float)iterations*4.0; 
//**END**// 
 
//Do stuff here with your occlusion value “ao”: modulate ambient lighting, write it to a buffer for later //use, etc. 
return o; 
}

Эта техника очень напоминает технику описанную в "Hardware Accelerated Ambient Occlusion Techniques on GPUs". Основные различия в структуре выборки и функции AO. Он также может рассматриваться как Image-Space версия  "Hardware Accelerated Ambient Occlusion Techniques on GPUs". Некоторые пояснения по коду:

Радиус делится на p.z, для изменения масштаба в зависимости от расстояния до камеры. Если пропустить этот раздел, все точки на экране будут использовать же радиус выборки, и результат не будет учитывать перспективу.
В цикле for,
coord1 это первоначальная выборка координат на 90 °.
coord2 является те же координаты, повернутые на 45 °.

Результаты



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

Self-occlusion появляется, потому что традиционные алгоритмы формирования выборок внутри области вокруг каждого пикселя, поэтому на плоских поверхностях которые не «occluded», по меньшей мере, половина из выборок помечены как «occluded». Из за этого получается сероватый цвет для общей окклюзии. Белое хало вокруг объектов возникает, потому что в этих областях self-occlusion не выполняется. Таким образом избавление от self-occlusion на самом деле хорошо помогает избавиться от хало. При перемещении камеры результат окклюзии изменяется, если следовать этому методу. Если вы склоняетесь к качеству вместо скорости, можно использовать два или несколько проходов алгоритма (повторяющиеся цикл for в коде) с различными радиусами, один для захвата более глобальных AO, а другие для детализации небольших трещин.


Продолжаем

Я описал упрощенную реализацию SSAO, которая очень хорошо подходит для игр.
Однако ее легко улучшить если принимать во внимание скрытые от камеры поверхности, для получения более высокого качества. Это потребует три буфера: две позиции/глубина буферов (front/back faces) и один буфер нормалей. Но вы можете сделать это только с двумя буферами: хранить глубину front/back faces в красном и зеленом каналах буфера, соответственно, а затем восстановить позицию каждого из них. Таким образом у вас будет один буфер для позиций и второй буфер для нормалей. Таковы результаты при использовании 16 выборок для каждой позиции буфера:


left: front faces occlusion, right: back faces occlusion
Что бы применить это, нужно просто добавить вызова функции doAmbientOcclusion()
Это дополнительный код, который необходимо добавить:
внутри цикла for, добавьте эти вызовы:
ao += doAmbientOcclusionBack(i.uv,coord1*(0.25+0.125), p, n); 
ao += doAmbientOcclusionBack(i.uv,coord2*(0.5+0.125), p, n); 
ao += doAmbientOcclusionBack(i.uv,coord1*(0.75+0.125), p, n); 
ao += doAmbientOcclusionBack(i.uv,coord2*1.125, p, n);

Добавьте эти две функции шейдер: 

 float3 getPositionBack(in float2 uv) 

    return tex2D(g_buffer_posb,uv).xyz; 

float doAmbientOcclusionBack(in float2 tcoord,in float2 uv, in float3 p, in float3 cnorm) 

    float3 diff = getPositionBack(tcoord + uv) - p; 
   const float3 v = normalize(diff); 
   const float d = length(diff)*g_scale; 
   return max(0.0,dot(cnorm,v)-g_bias)*(1.0/(1.0+d)); 
}

Добавьте семлер с именем «g_buffer_posb», содержащий позицию backFaces (для его создания вам нужно рисовать сцену с передних граней).  Еще небольшое изменение, которое может помочь, на этот раз улучшить скорость, является добавление простой системы LOD (уровень детализации) в наш шейдер. Измените фиксированное количества итераций в соответствии с:

 int iterations = lerp(6.0,2.0,p.z/g_far_clip);

Переменная «g_far_clip» - это расстояние до дальней плоскости отсечения, которое должно быть передано в шейдер. Теперь количество итераций, применяемое к каждой точке зависит от расстояния до камеры. Таким образом для дальних точек выполнется меньше выборок, это позволяет повысить производительность без заметного ухучшения качества. Я не использовал это при измерении производительности (см. ниже), но все-же:




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


Left: the SSAO presented here, 48 samples per pixel (32 for front faces and 16 for back faces), no blur. Right: Ray traced AO in Mental Ray. 32 samples, spread = 2.0, maxdistance = 1.0; falloff = 1.0.

И совет на последок: не планируйте подключить шейдер в свой конвейер и автоматически получить реалистичный результат. Несмотря на эту реализацию, что-бы получить соотношение хорошей производительности и качества SSAO, вы должны настроить его как можно тщательнее для своих нужд и добиться максимальной производительности.
Добавление или удаление выборок и размытия, изменяя интенсивность и т.д. Вам следует также понять, подходит ли такой алгоритм SSAO для вас. Если у вас есть много динамических объектов в сцене, то возможно нет необходимости в SSAO вообще, может быть использование LightMap-ов достаточно для ваших целей, так- как они могут обеспечить лучшее качество для статических сцен. Я надеюсь, что вы извлекли пользу от этого метода. Весь код, представленный в этой статье доступен по mit-license.
Оригинал статьи можно найти здесь a-simple-and-practical-approach-to-ssao




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

Отправить комментарий

Physically Based Rendering