Screen-Space Ambient Occlusion.

Для получения действительно высококачественных изображений близких к реалистичным очень важно учитывать так называемое глобальное освещение (Global Illumination, GI) - рассеянное освещение падающее практически со всех сторон. Фактически любой объект рассеивает падающий на него свет и, таких образом, вносит свой вклад в освещение других объектов (и себя самого в том числе).

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

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

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

Ясно, что подобный подход для реальных сцен малоприменим. Однако существует его развитие, основанное на понятие Ambient Occlusion (AO, далее мы будем использовать термин затенение). Фактически Ambient Occlusion характеризует насколько данная точка закрыта близкорасположенными объектами (и, следовательно, как много рассеянного света она может получать).

Существует несколько вариантов Ambient Occlusion, но все они таки или иначе заключаются в определении того, какая часть полусферы, построенной вокруг заданной точки, закрыта близкорасположенными объектами (рис 1).

Рис 1. Разные случаи для "закрывания" окрестности точки.

Так на рисунке 1а) точка совсем не закрыта и свет на нее может падать со всех направлений из положительного полупространства. В случаях 1б) и 1в) достаточно большая часть окрестности точки закрыта близкорасположенными объектами и это уменьшает число направлений, откуда на нее может падать рассеянный свет.

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

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

Через S+ здесь обозначена верхняя полусфера, описанная вокруг точки x, а функция V(x,y) принимает значение, равное единице тогда, кода точки x и y видны друг из друга (т.е. соединяющий их отрезок не пересекает никаких объектов). Иначе значение функции равно нулю.

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

Здесь для учета расстояния используется специальная функция, на которую накладываются следующие требования:

Простейшим примером такой функции является следующая:

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

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

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

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

Рис 2. Использование полукуба вместо полусферы.

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

Тем самым достаочно всего лишь 5 раз отрендерить окрестность каждой точки и осуществить еще один проход шейдера для вычисления затенения. Существует готовый пакет, предоставляющий такую возможность - Fast Ambient Occlusion Generator.

Сейчас стало довольно распространенным вычисление затенения для каждой вершины статической геометрии сцены (правда для этого в ряде случаев требуется дополнительная тесселяция геометрии для обеспечения достаточной плотности вершин).

Помимо расчета самой затененности довольно часто рассчитывается и так называемая bent normal - среднее значение нормали по всем видимым ("не заблокированным") направлениям (рис 3).

Рис 3. Bent normal для случаев с рис. 1.

Далее это значение может использоваться вместо обычной нормали при расчете освещенности позволяя учитывать затененность и при расчете непосредственного освещения. Таким образом достаточно хранить всего четыре числа для каждой вершины - Ambient Occlusion и три компоненты bent normal.

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

Одним из наиболее известных подобных методов является так называемое Screen-Space Ambient Occlusion (SSAO, иногда также используется термин ISAO - Image Space Ambient Occlusion), впервые использованная в игре Crysis.

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

Для этого достаточно для каждого пиксела восстановить исходные трехмерные координаты соответствующей точки (в системе координат камеры) (xeye, yeye, zeye), после чего выбирается некоторая окрестность этой точки и в ней набор точек (xi, yi, zi),i=1,...,N. Далее для каждой из них можно проверить их видимость из (xeye, yeye, zeye) и получить приближенное значение затенения.

Рис 4. Нахождение затенения точки изображения по буферу глубины.

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

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

Рис 5. Не попавший в буфер глубины объект А может оказывать большое влияние на затенение точки Р (в отличии от закрывающей его грани слишком далеко расположенной от точки Р).

Однако явная трассировка лучей к каждой выбранной в окрестности точке хотя и верна с точки зрения исходной формулы и вполне реализуема на основе данных из буфера глубины (получается просто трассировка лучей в height field'е), но является все же явным overkill'ом.

На практике (и в частности в Crysis'е) используется гораздо более простой подход - каждая из точек из окрестности (xi, yi, zi) проектируется на буфер глубины (т.е. для каждой такой точки определяется точка (x'i, y'i, z'i) исходной сцены, проектирующаяся в тот же пиксел экрана)и полученная проекция (x'i, y'i, z'i) трактуется как некоторый локальный загораживатель (occluder) интересующей нас точки (xeye, yeye, zeye). Тогда приближенное значение загораживания может быть посчитано как просто как сумма загораживаний для каждой из этих точек.

При этом степень загораживания для точки обычно является функцией разности dz=zeye-z'i для интересующей нас точки (xeye, yeye, zeye) и данной точки (x'i, y'i, z'i).

Рис 6. Проектирование точек на буфер глубины

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

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

Простейшая реализация данного подхода должна учесть некоторые детали. Количество точек-образцов их отличие от исходной точки обычно постоянно и хранится в массиве (в приведенных листингах для этого используется массив rndTable).

Однако использование одних и тех же смещений без какой-либо рандомизации для расчета затенения приведет к появлению заметного aliasing'а. Для борьбы с ним вполне подходит подход, использованный в Crysis'е - в специальной текстуре (обычно небольшого размера, например 4х4) хранятся псевдослучайные трехмерные вектора. Тем самым каждому пикселу сопоставляется такой псевдослучайный единичный вектор.

Рис 7. Увеличенный пример текстуры с псевдослучайными векторами из Crysis'а.

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

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

Ниже приводится вариант вершинного и фрагментного шейдера, реализующих данный подход:

//
// SSAO vertex shader
//

varying vec3 pos;

void main(void)
{
    pos = vec3 ( gl_ModelViewMatrix * gl_Vertex );          // transformed point to world space

    gl_Position     = gl_ModelViewProjectionMatrix * gl_Vertex;
    gl_TexCoord [0] = gl_MultiTexCoord0;
}

//
// SSAO fragment shader
//

uniform sampler2D   depthMap;
uniform sampler2D   rotateMap;

varying vec3 pos;

void main (void)
{
    const float zFar     = 100.0;
    const float zNear    = 0.01;
    const float radius   = 0.2;
    const float attBias  = 0.3;
    const float attScale = 1.0;
    
	vec4	rndTable [8] = vec4 [8] 
	(
		vec4 ( -0.5, -0.5, -0.5, 0.0 ),
		vec4 (  0.5, -0.5, -0.5, 0.0 ),
		vec4 ( -0.5,  0.5, -0.5, 0.0 ),
		vec4 (  0.5,  0.5, -0.5, 0.0 ),
		vec4 ( -0.5, -0.5,  0.5, 0.0 ),
		vec4 (  0.5, -0.5,  0.5, 0.0 ),
		vec4 ( -0.5,  0.5,  0.5, 0.0 ),
		vec4 (  0.5,  0.5,  0.5, 0.0 )
	);
    
    float   zb    = texture2D ( depthMap, gl_TexCoord [0].xy ).x;
    float   z     = zFar*zNear/(zb * (zFar - zNear) - zFar);         // get z-eye
    float   att   = 0.0;
    vec3    plane = 2.0 * texture2D ( rotateMap, gl_TexCoord [0].xy * 512.0/4.0).xyz - vec3 ( 1.0 );
    
    for ( int i = 0; i < 8; i++ )
    {
        vec3    sample  = reflect   ( rndTable [i].xyz, plane );
        float   zSample = texture2D ( depthMap, gl_TexCoord [0].xy + radius*sample.xy / z ).x;

        zSample = zFar * zNear / (zSample * (zFar - zNear) - zFar );
        
        if ( zSample - z > 0.1 )
            continue;
            
        float   dz = max ( zSample - z, 0.0 ) * 30.0;
        
        att += 1.0 / ( 1.0 + dz*dz );
    }
    
    att = clamp ( (att / 8.0 + attBias) * attScale, 0.0, 1.0 );
    
    gl_FragColor = vec4 ( att );
}

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

Рис 8. Фоновое затенение для простой сцены.

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

Рис 9. Причина возникновения самозатенения плоских граней.

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

Чтобы этого избежать можно подвергнуть dz простому преобразованию:

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

//
// SSAO-2 fragment shader
//

uniform sampler2D   depthMap;
uniform sampler2D   rotateMap;
uniform float       radius;
uniform float       distScale;
uniform float       bias;

varying vec3 pos;

void main (void)
{
    const float zFar      = 100.0;
    const float zNear     = 0.01;
    const float attScale  = 1.0;
    
	vec4	rndTable [8] = vec4 [8] 
	(
		vec4 ( -0.5, -0.5, -0.5, 0.0 ),
		vec4 (  0.5, -0.5, -0.5, 0.0 ),
		vec4 ( -0.5,  0.5, -0.5, 0.0 ),
		vec4 (  0.5,  0.5, -0.5, 0.0 ),
		vec4 ( -0.5, -0.5,  0.5, 0.0 ),
		vec4 (  0.5, -0.5,  0.5, 0.0 ),
		vec4 ( -0.5,  0.5,  0.5, 0.0 ),
		vec4 (  0.5,  0.5,  0.5, 0.0 )
	);
    
    float   zb    = texture2D ( depthMap, gl_TexCoord [0].xy ).x;
    float   z     = zFar*zNear/(zb * (zFar - zNear) - zFar);          // get z-eye
    vec3    pe    = pos * z / pos.z;                                  // point in eye coordinates
    float   att   = 0.0;
    vec3    plane = 2.0 * texture2D ( rotateMap, gl_TexCoord [0].xy * 512.0/4.0).xyz - vec3 ( 1.0 );
    
    for ( int i = 0; i < 8; i++ )
    {
        vec3    sample  = reflect   ( rndTable [i].xyz, plane );
        float   zSample = texture2D ( depthMap, gl_TexCoord [0].xy + radius*sample.xy / z ).x;

        zSample = zFar * zNear / (zSample * (zFar - zNear) - zFar );
        
        float   dist = max ( zSample - z, 0.0 ) / distScale;    
        float   occl = 15 * max ( dist * (2.0 - dist), 0.0 );
        
        att += 1.0 / ( 1.0 + occl*occl );
    }
    
    att = clamp ( att / 8.0 + 0.45, 0.0, 1.0 ) * attScale;
    
    gl_FragColor = vec4 ( att );
}

Рис 10. Фоновое затенение для сцены с ящиками при использовании второго варианта шейдера.

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

Рис 11. Использование угла между нормалью и направлением на точку-образец.

Так точка A вносит вклад (для нее соответствующий угол острый), точки B и C вклада не вносят - для них углы не острые. Тем самым можно ввести дополнительный множитель в выражение, дающее затенение для точки, зависящее от данного угла в виде max(dot(n,v)+bias,0), где v - это вектор от точки, в которой расчитывается затенение, до точки-образца.

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

Ниже приводятся фрагментные шейдеры для первого (строящего G-буфер) и второго (рассчитывающего затенения) проходов.

//
// SSAO pass 1 - build G-buffer with eye-space coords
//

varying vec3 pos;
varying vec3 n;

uniform sampler2D   diffMap;

void main (void)
{
    vec3    n2   = normalize ( n );
    
    gl_FragData [0] = vec4 ( pos, gl_FragCoord.z );
    gl_FragData [1] = vec4 ( n, 1.0 );
}

//
// SSAO fragment shader pass 2 - compute AO
//

#extension GL_ARB_texture_rectangle : enable

uniform sampler2D       rotateMap;
uniform sampler2DRect   posMap;
uniform sampler2DRect   normalMap;

varying vec3 pos;

void main (void)
{
    const float zFar        = 20.0;
    const float zNear       = 0.01;
    const float radius      = 0.2;
    const float minCrease   = -0.1;
    const float creaseScale = 1.0;
    const float distScale   = 0.5;
    
	vec2 rndTable [12] = vec2 [12] 
	(
	   vec2 ( -0.326212, -0.405805 ),
	   vec2 ( -0.840144, -0.073580 ),
	   vec2 ( -0.695914,  0.457137 ),
	   vec2 ( -0.203345,  0.620716 ),
	   vec2 (  0.962340, -0.194983 ),
	   vec2 (  0.473434, -0.480026 ),
	   vec2 (  0.519456,  0.767022 ),
	   vec2 (  0.185461, -0.893124 ),
	   vec2 (  0.507431,  0.064425 ),
	   vec2 (  0.896420,  0.412458 ),
	   vec2 ( -0.321940, -0.932615 ),
	   vec2 ( -0.791559, -0.597705 )
	);
    
    vec3    pe    = texture2DRect ( posMap,    gl_FragCoord.xy ).xyz;
    vec3    ne    = texture2DRect ( normalMap, gl_FragCoord.xy ).xyz;
    float   att   = 0.0;
    vec3    plane = 2.0 * texture2D ( rotateMap, gl_FragCoord.xy/4.0).xyz - vec3 ( 1.0 );
    
    for ( int i = 0; i < 12; i++ )
    {
        vec2    sampleTex = gl_FragCoord.xy + 512.0 * radius * reflect ( vec3 ( rndTable [i], 0.0 ), plane ).xy / pe.z;
        vec3    sampleEye = texture2DRect ( posMap,  sampleTex ).xyz;
        vec3    toCenter  = sampleEye - pe;
        float   dist      = length ( toCenter );
        
        toCenter /= dist;                               // normalize
    
        float   normAtten  = clamp ( ( dot ( toCenter, ne ) - minCrease ) * creaseScale, 0.0, 1.0 );
        float   rangeAtten = 1.0 - clamp ( dist / distScale, 0.0, 1.0 );

        att += normAtten * rangeAtten;
    }
    
    gl_FragColor = vec4 ( pow ( (1.0 - att / 12.0)*1.1, 2.0 ) );
}

Рис. 12. Затенение, посчитанное с учетом нормалей.

Кроме рассмотренных способов вычисления SSAO есть еще один, используемый в Blender'е (см.. ссылки в конце статьи) - просто строится blurred буфер глубины, находится разница между blurred- и нормальным буфером глубины и отсекается по отрезку [0,1]. Этот способ работает, хотя он требует большего числа обращений к буферу глубины и в ряде случаев приводит к возникновению "хало" за счет размытия буфера глубины.

Рис 13. Затенение, полученное при помощи размытия буфера глубины.

Получаемое практически любым рассмотренным выше способом имеет "зернистый" характер и перед наложением на изображение должна быть "размыта" (сглажена). Ниже приводится исходный код для самого простого варианта сглаживания, более качественных результатов можно добиться увеличив размеры ядра сглаживания и (чтобы избежать появления "хало") учитывая при сглаживании только те пикселы, для которых dz>0.

Приводимый ниже вариант просто "в лоб" осуществляет сглаживание с небольшим размером ядра и накладывает его на исходное изображение (хотя вместо явного умножения можно было просто настроить alpha blending).

//
// SSAO - pass 2 - blur AO texture and apply to source image
//

uniform sampler2D   srcMap;
uniform sampler2D   aoMap;

void main (void)
{
    float   h1 = 0.5 / 512.0;           // half of a pixel (x)
    float   h2 = 0.5 / 512.0;           // half of a pixel (y)
    vec4    ao = vec4 ( 0.0 );
    
    for ( int i = -1; i <= 1; i++ )
        for ( int j = -1; j <= 1; j++ )
            ao += texture2D ( aoMap, gl_TexCoord [0].xy + vec2 (  (2*i+1)*h1, (2*j+1)*h2 ) );

    gl_FragColor = texture2D ( srcMap, gl_TexCoord [0].xy ) * pow ( ao / 9.0, vec4 ( 2.0 ) );
}

Ниже приводится исходный код соответствующей программы на С++.

#include    "libExt.h"

#include    <glut.h>
#include    <stdio.h>
#include    <stdlib.h>

#include    "libTexture.h"
#include    "TypeDefs.h"
#include    "Vector3D.h"
#include    "Vector2D.h"
#include    "boxes.h"
#include    "FrameBuffer.h"
#include    "GlslProgram.h"
#include    "utils.h"
#include    "Camera.h"
#include    "fpTexture.h"

Vector3D    eye   ( -0.7, -2.0, 1.0 );          // camera position
unsigned    stoneMap, woodMap, teapotMap, decalMap;
unsigned    depthMap;
unsigned    rotateMap;

float radius    = 0.12;
float distScale = 0.25;
float bias      = 0.45;

float   angle = 0;
float   yaw   = 0;
float   pitch = 0;
float   roll  = 0;

Camera      camera ( eye, 0, 0, 0, 90,  0.01 ); // camera to be used

FrameBuffer buffer  ( 512, 512, FrameBuffer :: depth32 );
FrameBuffer buffer2 ( 512, 512 );
GlslProgram program;                            // create AO texture                
GlslProgram program2;                           // blur AO texture and apply it to main image

void displayBoxes ();
void reshape      ( int w, int h );

inline  float   rnd ()
{
    return (float) rand () / (float) RAND_MAX;
}

inline  float   rnd ( float a, float b )
{
    return a + rnd () * (b - a);
}

void displayBoxes ()
{
    glMatrixMode ( GL_MODELVIEW );
    glPushMatrix ();

    glActiveTextureARB ( GL_TEXTURE0_ARB );
    
    drawBox  ( Vector3D ( -5, -5, 0 ), Vector3D ( 10, 10, 3 ), stoneMap, false );
    
    glActiveTextureARB ( GL_TEXTURE0_ARB );
    
    drawBox  ( Vector3D ( 3, 3, 0  ), Vector3D ( 1,    1,    1   ), decalMap, true );
    drawBox  ( Vector3D ( 4, 1, 0  ), Vector3D ( 1,    0.5,  1   ), decalMap, true );
    drawBox  ( Vector3D ( 4, -1, 0 ), Vector3D ( 1,    0.5,  1   ), decalMap, true );
    drawBox  ( Vector3D ( 3, -3, 0 ), Vector3D ( 0.75, 1.4,  1.2 ), decalMap, true );
    drawBox  ( Vector3D ( 2, 1, 0  ), Vector3D ( 1,    2.2,  1.5 ), decalMap, true );
    drawBox  ( Vector3D ( 1, -4, 0 ), Vector3D ( 0.7,  0.75, 1.2 ), decalMap, true );

    glActiveTextureARB ( GL_TEXTURE0_ARB );
    
    drawBox  ( Vector3D ( 1,  -2, 0.5 ),  Vector3D ( 1,  1,   1 ), woodMap, true );
    drawBox  ( Vector3D ( 0.7, 1, 0.5 ),  Vector3D ( 1,  1,   1 ), woodMap, true );
    drawBox  ( Vector3D ( -1.7, 0, 0.8 ), Vector3D ( 2,  1.5, 1 ), woodMap, true );
    
    glBindTexture   ( GL_TEXTURE_2D, teapotMap );
    glTranslatef    ( 1.3, 0.3, 0.7 );
    glRotatef       ( angle * 45.3, 1, 0, 0 );
    glRotatef       ( angle * 57.2, 0, 1, 0 );
    glutSolidTeapot ( 0.5 );

    glPopMatrix     ();
}

void display ()
{
                                                // render for FP FBO
    buffer.bind ();

    reshape ( buffer.getWidth (), buffer.getHeight () );
    
    glClear ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

    camera.apply ();
    displayBoxes ();

    glActiveTextureARB ( GL_TEXTURE1_ARB );
    glBindTexture      ( GL_TEXTURE_2D, rotateMap );
    glActiveTextureARB ( GL_TEXTURE0_ARB );
    glBindTexture      ( GL_TEXTURE_2D, depthMap );
    glCopyTexImage2D   ( GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, 0, 0, 512, 512, 0 );

    buffer.unbind ();
    
    buffer2.bind ();
    program.bind ();
    startOrtho ( 512, 512 );
    drawQuad   ( 512, 512 );
    endOrtho   ();
    buffer2.unbind ();
    program.unbind ();
    
    glActiveTextureARB ( GL_TEXTURE1_ARB );
    glBindTexture      ( GL_TEXTURE_2D, buffer2.getColorBuffer () );
    glActiveTextureARB ( GL_TEXTURE0_ARB );
    glBindTexture      ( GL_TEXTURE_2D, buffer.getColorBuffer () );
    
    program2.bind ();
    startOrtho ( 512, 512 );
    drawQuad   ( 512, 512 );
    endOrtho   ();
    program2.unbind ();
    
    glutSwapBuffers ();
}

void reshape ( int w, int h )
{
    camera.setViewSize ( w, h, 60 );
    camera.apply       ();
}

void key ( unsigned char key, int x, int y )
{
    if ( key == 27 || key == 'q' || key == 'Q' )        // quit requested
        exit ( 0 );
    else
    if ( key == 'w' || key == 'W' )
        camera.moveBy ( camera.getViewDir () * 0.2 );
    else
    if ( key == 'x' || key == 'X' )
        camera.moveBy ( -camera.getViewDir () * 0.2 );
    else
    if ( key == 'a' || key == 'A' )
        camera.moveBy ( -camera.getSideDir () * 0.2 );
    else
    if ( key == 'd' || key == 'D' )
        camera.moveBy ( camera.getSideDir () * 0.2 );
    else
    if ( key == '+' )
        distScale += 0.05;
    else
    if ( key == '-' )
        distScale -= 0.05;
    else
    if ( key == '*' )
        bias += 0.01;
    else
    if ( key == '/' )
        bias -= 0.01;
    else
    if ( key == '[' )
        radius += 0.02;
    else
    if ( key == ']' )
        radius -= 0.02;

    program.bind            ();
    program.setUniformFloat ( "radius",    radius );
    program.setUniformFloat ( "distScale", distScale );
    program.setUniformFloat ( "bias",      bias );
    program.unbind          ();
    
    glutPostRedisplay ();
}

void    specialKey ( int key, int x, int y )
{
    if ( key == GLUT_KEY_UP )
        yaw += M_PI / 90;
    else
    if ( key == GLUT_KEY_DOWN )
        yaw -= M_PI / 90;
    else
    if ( key == GLUT_KEY_RIGHT )
        roll += M_PI / 90;
    else
    if ( key == GLUT_KEY_LEFT )
        roll -= M_PI / 90;

    camera.setEulerAngles ( yaw, pitch, roll );

    glutPostRedisplay ();
}

void    mouseFunc ( int x, int y )
{
    static  int lastX = -1;
    static  int lastY = -1;

    if ( lastX == -1 )              // not initialized
    {
        lastX = x;
        lastY = y;
    }

    yaw  -= (y - lastY) * 0.02;
    roll += (x - lastX) * 0.02;

    lastX = x;
    lastY = y;

    camera.setEulerAngles ( yaw, pitch, roll );

    glutPostRedisplay ();
}

void    animate ()
{
    static  float   lastTime = 0.0;
    float           time     = 0.001f * glutGet ( GLUT_ELAPSED_TIME );

    angle   += 2 * (time - lastTime);
    lastTime = time;

    glutPostRedisplay ();
}

int main ( int argc, char * argv [] )
{
                                // initialize glut
    glutInit            ( &argc, argv );
    glutInitDisplayMode ( GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH );
    glutInitWindowSize  ( buffer.getWidth (), buffer.getHeight () );

                                // create window
    glutCreateWindow ( "Screen-Space Ambient Occlusion Demo" );

                                // register handlers
    glutDisplayFunc       ( display    );
    glutReshapeFunc       ( reshape    );
    glutKeyboardFunc      ( key        );
    glutSpecialFunc       ( specialKey );
    glutPassiveMotionFunc ( mouseFunc  );
    glutIdleFunc          ( animate    );

    init           ();
    initExtensions ();

    assertExtensionsSupported ( "EXT_framebuffer_object" );

    rotateMap = createTexture2D ( false, "rotate.bmp" );
    teapotMap = createTexture2D ( true, "../../Textures/Oxidated.jpg" );
    woodMap   = createTexture2D ( true, "../../Textures/oak.bmp" );
    stoneMap  = woodMap;
    decalMap  = woodMap;
    
    glGenTextures   ( 1, &depthMap );
    glBindTexture   ( GL_TEXTURE_2D, depthMap );
    glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP );
    glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP );
    glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
    glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );

    glTexImage2D     ( GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, 512, 512, 0,
                       GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, NULL );
    
    
    unsigned screenMap = buffer.createColorTexture ( GL_RGBA, GL_RGBA8 );
    
    buffer.create ();
    buffer.bind   ();
    
    if ( !buffer.attachColorTexture ( GL_TEXTURE_2D, screenMap, 0 ) )
        printf ( "buffer error with color attachment\n");

    if ( !buffer.isOk () )
        printf ( "Error with framebuffer\n" );
                    
    buffer.unbind ();

    screenMap = buffer2.createColorTexture ( GL_RGBA, GL_RGBA8 );
    
    buffer2.create ();
    buffer2.bind   ();
    
    if ( !buffer2.attachColorTexture ( GL_TEXTURE_2D, screenMap, 0 ) )
        printf ( "buffer2 error with color attachment\n");

    if ( !buffer2.isOk () )
        printf ( "Error with framebuffer2\n" );
                    
    buffer2.unbind ();

    if ( !program.loadShaders ( "ssao-4-p1.vsh", "ssao-4-p1.fsh" ) )
    {
        printf ( "Error loading shaders:\n%s\n", program.getLog ().c_str () );

        return 3;
    }
    
    if ( !program2.loadShaders ( "ssao-4-p2.vsh", "ssao-4-p2.fsh" ) )
    {
        printf ( "Error loading shaders:\n%s\n", program2.getLog ().c_str () );

        return 3;
    }
    
    program.bind            ();
    program.setTexture      ( "depthMap",  0 );
    program.setTexture      ( "rotateMap", 1 );
    program.setUniformFloat ( "radius",    radius );
    program.setUniformFloat ( "distScale", distScale );
    program.setUniformFloat ( "bias",      bias );
    program.unbind          ();

    program2.bind ();
    program2.setTexture  ( "srcMap", 0 );
    program2.setTexture  ( "aoMap",  1 );
    program2.unbind      ();

    camera.setRightHanded ( false );

    printf ( "Use:\n\t+ and - keys to change distScale\n\t* and / to change bias\n\t[ and ] to change radius." );
    
    glutMainLoop ();

    return 0;
}

На следующих скриншотах показывается результат наложения затенения на модельную сцену.

Рис. 14. Сцена с ящиками выведенная описанным выше методом.

Рис. 15. Сцена с ящиками выведенная описанным выше методом.

По этой ссылке можно скачать весь исходный код к этой статье. Также доступны для скачивания откомпилированные версии для M$ Windows и Linux.

Также ниже приводится ряд полезных ссылок на статьи и обсуждения SSAO.

Wikipedia

Хороший обзор различных подходов

Статья из GDC08 (pdf)

Hardware Accelerated Ambient Occlusion Techniques on GPUs (pdf)

Обсуждение на Gamedev.net

Хорошая статья с объяснением как это было сделано в Kindernoiser

Подход с blur'ом буфера глубины

Ambient Occlusive Crease Shading

Псевдокод алгоритма, использованного в 3DS Max

Разбор шейдеров в Crysis'е

Обсуждение на Gamedev.ru

Еще одно обсуждение на Gamedev.ru