Deferred Shading.

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

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

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

С подобными проблемами столкнулись разработчики игры S.T.A.L.K.E.R. - с самого начала уровни игры были очень сильно детализированы и требовалось поддерживать много источников света.

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

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

После этого делается второй проход, который осуществляет расчет освещенности на основе построенного G-буфера и свойств источников света.

Таким образом мы полностью отделяем геометрическую сложность сцены от количества источников света. Сложность сцены играет роль только на этапе построения G-буфера.

Хранимая в G-буфере информация обычно состоит из 3D-координат точки сцены, соответствующей данному пикселу (в какой-либо системе координат, обычно используется система координат связанная с камерой), вектора нормали (также в некоторой системе координат) и цветов (диффузного, бликового и т.п.). Также может включаться и дополнительная информация (ambient occlusion, светимость, свойства материала и т.п.).

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

Таблица 1. Базовый вариант раскладки G-буфера.

Величина Комментарий
(x,y,z) Координаты исходной точки
(nx,ny,nz) Единичный вектор нормали в точке
(dred,dgreen,dblue) Диффузный цвет
(sred,sgreen,sblue) Бликовый цвет

Для хранения всей этой информации очень удобно использовать возможность рендеринга сразу в несколько текстур (MRT). GPU серии GeForce 6xxx и выше позволяют одновременно осуществлять рендеринг в четыре текстуры, но при этом существует ограничение - каждая из этих текстур должна содержать одинаковое число бит на пиксел (как например форматы GL_RGBA8 и GL_ALPHA32F_ARB, которые содержат по 32 бита на пиксел).

Благодаря поддержке floating-point-форматов возможно сохранять все данные с необходимой точностью (для хранения координат обычно требуется 32 бита, т.е. полноценный тип float).

Рассмотрим сначала самый простой случай - будем осуществлять рендеринг всего в две текстуры формата GL_RGBA32F_ARB.

Таблица 2.

Номер текстуры Red Green Blue Alpha
0 xeye yeye zeye не используется
1 nx,eye ny,eye nz,eye не используется

Хотя использование формата GL_RGBA32F_ARB для хранения нормали явно избыточно, но задавать координаты необходимо с максимальной точностью (32 бита на компоненту).

Ниже приводятся вершинный и фрагментный шейдеры для записи в такой G-буфер.

//
// Vertex Shader for deferred shading pass 1
//
varying vec3 pos;
varying vec3 n;

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

    gl_Position     = gl_ModelViewProjectionMatrix * gl_Vertex;
}

//
// Fragment Shader for deferred shading pass 1
//
varying vec3 pos;
varying vec3 n;

void main (void)
{
    vec3    n2   = normalize ( n );
    
    gl_FragData [0] = vec4 ( pos, gl_FragDepth );
    gl_FragData [1] = vec4 ( 0.5*n2 + vec3(0.5), 1.0 );
}

На следующем скриншоте приводится содержимое буфера с вектором нормали (представленным в виде цвета).

Рис 1. Содержимое буфера с вектором нормали.

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

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

Соответствующие вершинный и фрагментные шейдеры приведены ниже.

//
// Vertex Shader for deferred shading pass 2
//

void main(void)
{
    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}

//
// Fragment Shader for deferred shading pass 2
//

#extension GL_ARB_texture_rectangle: enable

uniform vec3 lightPos;
uniform sampler2DRect   posMap;
uniform sampler2DRect   normalMap;

void main (void)
{
    vec3    p  = texture2DRect ( posMap,    gl_FragCoord.xy ).xyz;
    vec3    n  = texture2DRect ( normalMap, gl_FragCoord.xy ).xyz;
    vec3    l  = normalize     ( lightPos - p );
    vec3    v  = normalize     ( -p );
    vec3    h  = normalize     ( l + v );
    float   diff = max         ( 0.2, dot ( l, n ) );
    float   spec = pow         ( max ( 0.0, dot ( h, n ) ), 40.0 );
    
    gl_FragColor = vec4 ( vec3 ( diff + spec ), 1.0 );
}

Рис 2. Полученная картинка.

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

Остановимся на последнем подробнее. Дело в том, что при обычном (forward) рендеринге все направления переводятся в касательное (tangent) пространство и весь расчет освещение происходит именно в нем. У нас ситуация иная - нам нужно перевести нормаль из касательного пространства (в котором нормали заданы в карте нормалей) в пространство камеры (eye/camera space).

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

//
// Fragment shader for deferred shading pass 1
//

varying vec3 pos;
varying vec3 n;
varying vec3 t;
varying vec3 b;

uniform sampler2D   diffMap;
uniform sampler2D   bumpMap;

void main (void)
{
    vec3    nn = 2.0*texture2D ( bumpMap, gl_TexCoord [0].xy ).xyz - vec3 ( 1.0 );
    
    gl_FragData [0] = vec4      ( pos, gl_FragDepth );
    gl_FragData [1] = vec4      ( normalize ( nn.x * t + nn.y * b + nn.z * n ), 1.0 );
    gl_FragData [2] = texture2D ( diffMap, gl_TexCoord [0].xy );
}

Рис 3. Изображение из буфера с нормалями при использовании bumpmap'а.

В результате мы получим в G-буфере вектора нормали, заданные в пространстве камеры, с учетом карты нормалей, диффузный цвет и координаты точек.

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

//
// Fragment shader for deferred shading pass 2
//
#extension GL_ARB_texture_rectangle: enable

uniform vec3 lightPos;

uniform sampler2DRect   posMap;
uniform sampler2DRect   normalMap;
uniform sampler2DRect   colorMap;

void main (void)
{
    vec3    p  = texture2DRect ( posMap,    gl_FragCoord.xy ).xyz;
    vec3    n  = texture2DRect ( normalMap, gl_FragCoord.xy ).xyz;
    vec3    c  = texture2DRect ( colorMap,  gl_FragCoord.xy ).xyz;
    vec3    l  = normalize     ( lightPos - p );
    vec3    v  = normalize     ( -p );
    vec3    h  = normalize     ( l + v );
    float   diff = max         ( 0.2, dot ( l, n ) );
    float   spec = pow         ( max ( 0.0, dot ( h, n ) ), 40.0 );
    
    gl_FragColor = vec4 ( diff * c + vec3 ( spec ), 1.0 );
}

Ниже приводится получаемое при таком подходе изображение.

Рис 4. Изображение сцены с использованным bumpmap'ом.

Однако использование в качестве G-буфера трех текстур формата GL_RGBA32F_ARB весьма дорогостояще - G занимает много памяти а это ведет к сильному увеличению fillrate'а. Существуют способы уменьшить размер памяти, отводимый в G-буфере на один пиксел. Наиболее важным местом является то, что вместо хранения всех трех координат точки (x,y,z) нам достаточно хранить всего одну - z (заданную в пространстве камеры) - две остальные легко могут быть вычислены по ней и координатам фрагмента (тексела).

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

Тем самым мы приходим к тому, что в качестве формата текстуры для хранения координат можно взять GL_ALPHA32F_ARB или GL_INTENSITY_32F_ARB (или 16-битовые GL_ALPHA_16F_ARB или GL_INTENSITY_16F_ARB).

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

Для хранения нормали существует два способа - можно либо использовать формат GL_RGBA8 (альфа-канал свободен и может быть использован для других целей), либо использовать двухкомпонентный floating-point формат, например GL_RG16F_ARB. Тогда мы в ней будем первые две компоненты вектора нормали, а последняя легко находится из условия нормировки.

Считается что последний способ обеспечивает лучшие визуальные результаты.

Рис 5. Варианты раскладки G-буфера.

За счет этих изменений нам удалось в 4 раза сократить объем памяти под G-буфер (а значит и соответственно уменьшить fillrate).

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

//
// Fragment shader for deferred shading pass 1
//

varying	vec3 pos;
varying	vec3 n;
varying	vec3 t;
varying	vec3 b;

uniform sampler2D	diffMap;
uniform sampler2D	bumpMap;

void main (void)
{
    vec3 nn = 2.0*texture2D ( bumpMap, gl_TexCoord [0].xy ).xyz - vec3 ( 1.0 );
	
    gl_FragData [0] = vec4 ( pos.z );
    gl_FragData [1] = vec4 ( normalize ( nn.x * t + nn.y * b + nn.z * n ), 1.0 );
    gl_FragData [2] = texture2D ( diffMap, gl_TexCoord [0].xy );
}

//
// Fragment shader for deferred shading pass 2
//
#extension GL_ARB_texture_rectangle: enable

varying	vec3 pos;
uniform vec3 lightPos;
uniform sampler2DRect posMap;
uniform sampler2DRect normalMap;
uniform sampler2DRect colorMap;

void main (void)
{
    float z  = texture2DRect ( posMap,    gl_FragCoord.xy ).r;
    vec3  n  = texture2DRect ( normalMap, gl_FragCoord.xy ).xyz;
    vec3  c  = texture2DRect ( colorMap,  gl_FragCoord.xy ).xyz;
    vec3  pp = pos * z / pos.z;
    vec3  l  = normalize ( lightPos - pp );
    vec3  v  = normalize ( -pp );
    vec3  h  = normalize ( l + v );
    float diff = max     ( 0.2, dot ( l, n ) );
    float spec = pow     ( max ( 0.0, dot ( h, n ) ), 40.0 );

    gl_FragColor = vec4 ( diff * c + vec3 ( spec ), 1.0 );
}

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

//
// Example of deferred shading
//
// Author: Alex V. Boreskoff <steps3d@narod.ru>
//

#include    "libExt.h"

#ifdef  MACOSX
    #include    <GLUT/glut.h>
#else
    #include    <glut.h>
#endif

#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"

Vector3D    eye   ( -0.5, -0.5, 1.5 );  // camera position
Vector3D    light ( -0.5, -0.5, 1.5 );      // light position
unsigned    stoneMap, woodMap, teapotMap, decalMap;
unsigned    bumpMap, bumpMap2, bumpMap3;
GLenum      format1, format2;

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 );
GlslProgram program1;                       // build G-buffer
GlslProgram program2;                       // test G-buffer

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

void    drawBoxTBN ( const Vector3D& pos, const Vector3D& size, unsigned texture, bool cull )
{
    float   x2 = pos.x + size.x;
    float   y2 = pos.y + size.y;
    float   z2 = pos.z + size.z;
    float   ns = cull ? 1 : -1;

    glBindTexture ( GL_TEXTURE_2D, texture );
    glEnable      ( GL_TEXTURE_2D );

    if ( cull )
    {
        glCullFace ( GL_BACK );
        glEnable   ( GL_CULL_FACE );
    }
    else
        glDisable ( GL_CULL_FACE );

    glBegin ( GL_QUADS );
                                    // front face
        glNormal3f         ( 0, 0, ns );
        glMultiTexCoord3fv ( GL_TEXTURE1_ARB, Vector3D ( 1, 0, 0 ) );
        glMultiTexCoord3fv ( GL_TEXTURE2_ARB, Vector3D ( 0, 1, 0 ) );

        glTexCoord2f ( 0, 0 );
        glVertex3f   ( pos.x, pos.y, z2 );

        glTexCoord2f ( size.x, 0 );
        glVertex3f   ( x2, pos.y, z2 );

        glTexCoord2f ( size.x, size.y );
        glVertex3f   ( x2, y2, z2 );

        glTexCoord2f ( 0, size.y );
        glVertex3f   ( pos.x, y2, z2 );

                                    // back face
        glNormal3f ( 0, 0, -ns );
        glMultiTexCoord3fv ( GL_TEXTURE1_ARB, Vector3D ( -1, 0, 0 ) );
        glMultiTexCoord3fv ( GL_TEXTURE2_ARB, Vector3D ( 0, -1, 0 ) );

        glTexCoord2f ( size.x, 0 );
        glVertex3f   ( x2, pos.y, pos.z );

        glTexCoord2f ( 0, 0 );
        glVertex3f   ( pos.x, pos.y, pos.z );

        glTexCoord2f ( 0, size.y );
        glVertex3f   ( pos.x, y2, pos.z );

        glTexCoord2f ( size.x, size.y );
        glVertex3f   ( x2, y2, pos.z );

                                    // left face
        glNormal3f ( -ns, 0, 0 );
        glMultiTexCoord3fv ( GL_TEXTURE1_ARB, Vector3D ( 0, 0, -1 ) );
        glMultiTexCoord3fv ( GL_TEXTURE2_ARB, Vector3D ( 0, -1, 0 ) );

        glTexCoord2f ( 0, 0 );
        glVertex3f   ( pos.x, pos.y, pos.z );

        glTexCoord2f ( 0, size.z );
        glVertex3f   ( pos.x, pos.y, z2 );

        glTexCoord2f ( size.y, size.z );
        glVertex3f   ( pos.x, y2, z2 );

        glTexCoord2f ( size.y, 0 );
        glVertex3f   ( pos.x, y2, pos.z );

                                    // right face
        glNormal3f ( ns, 0, 0 );
        glMultiTexCoord3fv ( GL_TEXTURE1_ARB, Vector3D ( 1, 0, 0 ) );
        glMultiTexCoord3fv ( GL_TEXTURE2_ARB, Vector3D ( 0, 1, 0 ) );

        glTexCoord2f ( 0, size.z );
        glVertex3f   ( x2, pos.y, z2 );

        glTexCoord2f ( 0, 0 );
        glVertex3f   ( x2, pos.y, pos.z );

        glTexCoord2f ( size.y, 0 );
        glVertex3f   ( x2, y2, pos.z );

        glTexCoord2f ( size.y, size.z );
        glVertex3f   ( x2, y2, z2 );

                                    // top face
        glNormal3f ( 0, ns, 0 );
        glMultiTexCoord3fv ( GL_TEXTURE1_ARB, Vector3D ( 1, 0, 0 ) );
        glMultiTexCoord3fv ( GL_TEXTURE2_ARB, Vector3D ( 0, 0, 1 ) );

        glTexCoord2f ( 0, size.z );
        glVertex3f   ( pos.x, y2, z2 );

        glTexCoord2f ( size.x, size.z );
        glVertex3f   ( x2, y2, z2 );

        glTexCoord2f ( size.x, 0 );
        glVertex3f   ( x2, y2, pos.z );

        glTexCoord2f ( 0, 0 );
        glVertex3f   ( pos.x, y2, pos.z );

                                    // bottom face
        glNormal3f ( 0, -ns, 0 );
        glMultiTexCoord3fv ( GL_TEXTURE1_ARB, Vector3D ( -1, 0, 0 ) );
        glMultiTexCoord3fv ( GL_TEXTURE2_ARB, Vector3D ( 0, 0, -1 ) );

        glTexCoord2f ( size.x, size.z );
        glVertex3f   ( x2, pos.y, z2 );

        glTexCoord2f ( 0, size.z );
        glVertex3f   ( pos.x, pos.y, z2 );

        glTexCoord2f ( 0, 0 );
        glVertex3f   ( pos.x, pos.y, pos.z );

        glTexCoord2f ( size.x, 0 );
        glVertex3f   ( x2, pos.y, pos.z );

    glEnd ();

    if ( cull )
        glDisable ( GL_CULL_FACE );
}

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

    glActiveTextureARB ( GL_TEXTURE1_ARB );
    glBindTexture      ( GL_TEXTURE_2D, bumpMap );
    glActiveTextureARB ( GL_TEXTURE0_ARB );
    
    drawBoxTBN  ( Vector3D ( -5, -5, 0 ), Vector3D ( 10, 10, 3 ), stoneMap, false );
    
    glActiveTextureARB ( GL_TEXTURE1_ARB );
    glBindTexture      ( GL_TEXTURE_2D, bumpMap2 );
    glActiveTextureARB ( GL_TEXTURE0_ARB );
    
    drawBoxTBN  ( Vector3D ( 3, 2, 0.5  ), Vector3D ( 1,  2,  2    ), decalMap, true );
    drawBoxTBN  ( Vector3D ( 3, -3, 0.5 ), Vector3D ( 2,  2,  1.75 ), decalMap, true );

    glActiveTextureARB ( GL_TEXTURE1_ARB );
    glBindTexture      ( GL_TEXTURE_2D, bumpMap3 );
    glActiveTextureARB ( GL_TEXTURE0_ARB );
    
    drawBoxTBN  ( Vector3D ( -3, -2, 0.5 ), Vector3D ( 1,  1,  1 ), woodMap, true );
    drawBoxTBN  ( Vector3D ( -3, 2, 0.5 ),  Vector3D ( 1,  1,  1 ), woodMap, true );
    
    glBindTexture   ( GL_TEXTURE_2D, teapotMap );
    glTranslatef    ( 0.2, 1, 1.5 );
    glRotatef       ( angle * 45.3, 1, 0, 0 );
    glRotatef       ( angle * 57.2, 0, 1, 0 );
    glutSolidTeapot ( 0.3 );

    glPopMatrix     ();
}

void display ()
{
    GLenum  buffers [] = { GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT, GL_COLOR_ATTACHMENT2_EXT };
    
                                                // render for FP FBO
    buffer.bind ();

    glDrawBuffers ( 3, buffers );
    
    reshape ( buffer.getWidth (), buffer.getHeight () );
    
    program1.bind ();

    glClear ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

    camera.apply ();
    displayBoxes ();

    program1.unbind ();
    buffer.unbind   ();
    
                        // now we have two buffers with data 0 - (x,y,z,gl_FragDepth), 1 - n
    
    Vector3D    p [4];
    camera.getPlanePolyForZ ( 1, p );
    
    glClear     ( GL_COLOR_BUFFER_BIT );
    glDisable   ( GL_DEPTH_TEST );
    glDepthMask ( GL_FALSE );
    glActiveTextureARB ( GL_TEXTURE2_ARB );
    glBindTexture      ( GL_TEXTURE_RECTANGLE_ARB, buffer.getColorBuffer ( 2 ) );

    glActiveTextureARB ( GL_TEXTURE1_ARB );
    glBindTexture      ( GL_TEXTURE_RECTANGLE_ARB, buffer.getColorBuffer ( 1 ) ); 

    glActiveTextureARB ( GL_TEXTURE0_ARB );
    glBindTexture      ( GL_TEXTURE_RECTANGLE_ARB, buffer.getColorBuffer ( 0 ) ); 
    
    program2.bind ();
    program2.setUniformVector ( "lightPos", camera.mapFromWorld ( light ) );
    
    glBegin ( GL_QUADS );
        glTexCoord2f ( buffer.getWidth (), buffer.getHeight () );
        glVertex3fv  ( p [0] );
        glTexCoord2f ( buffer.getWidth (), 0 );
        glVertex3fv  ( p [1] );
        glTexCoord2f ( 0, 0 );
        glVertex3fv  ( p [2] );
        glTexCoord2f ( 0, buffer.getHeight ()  );
        glVertex3fv  ( p [3] );
    glEnd ();
    
    program2.unbind ();
    
    glEnable    ( GL_DEPTH_TEST );
    glDepthMask ( GL_TRUE );
    
    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 );

    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 ( "OpenGL Deferred Shading example" );

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

    init           ();
    initExtensions ();

    assertExtensionsSupported ( "EXT_framebuffer_object" );

    decalMap  = createTexture2D ( true, "wood.png" );
    stoneMap  = createTexture2D ( true, "brick.tga" );
    teapotMap = createTexture2D ( true, "../../Textures/Oxidated.jpg" );
    bumpMap   = createTexture2D ( true, "brick_nm.bmp" );
    bumpMap2  = createTexture2D ( true, "wood_normal.png" );
    woodMap   = createTexture2D ( true, "wall.jpg" );
    bumpMap3  = createTexture2D ( true, "wall_bump.tga" );
    
    if ( isExtensionSupported ( "GL_ARB_texture_float" ) )
    {
        format1 = GL_INTENSITY32F_ARB;
        format2 = GL_RGBA8;
    }
    else
    if ( isExtensionSupported ( "GL_NV_float_buffer" ) )
    {
        format1 = GL_FLOAT_R32_NV;
        format2 = GL_RGBA8;
    }
    else
    if ( isExtensionSupported ( "GL_ATI_texture_float" ) )
    {
        format1 = GL_ALPHA_FLOAT32_ATI;
        format2 = GL_RGBA8;
    }
    else
    {
        printf ( "Floating-point textures not supported !\n" );
        
        return 1;
    }
    
    unsigned screenMap0 = buffer.createColorRectTexture ( GL_RGBA, format1 );
    unsigned screenMap1 = buffer.createColorRectTexture ( GL_RGBA, format2 );
    unsigned screenMap2 = buffer.createColorRectTexture ( GL_RGBA, format2 );
    
    buffer.create ();
    buffer.bind   ();
    
    if ( !buffer.attachColorTexture ( GL_TEXTURE_RECTANGLE_ARB, screenMap0, 0 ) )
        printf ( "buffer error with color attachment\n");

    if ( !buffer.attachColorTexture ( GL_TEXTURE_RECTANGLE_ARB, screenMap1, 1 ) )
        printf ( "buffer error with color attachment\n");

    if ( !buffer.attachColorTexture ( GL_TEXTURE_RECTANGLE_ARB, screenMap2, 2 ) )
        printf ( "buffer error with color attachment\n");

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

    if ( !program1.loadShaders ( "ex5-p1.vsh", "ex5-p1.fsh" ) )
    {
        printf ( "Error loading pass 1 shaders:\n%s\n", program1.getLog ().c_str () );

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

        return 3;
    }
    
    program1.bind ();
    program1.setTexture  ( "diffMap", 0 );
    program1.setTexture  ( "bumpMap", 1 );
    program1.unbind      ();

    program2.bind ();
    program2.setTexture  ( "posMap",    0 );
    program2.setTexture  ( "normalMap", 1 );
    program2.setTexture  ( "colorMap",  2 );
    program2.unbind      ();

    camera.setRightHanded ( false );
    
    glutMainLoop ();

    return 0;
}

Существует еще два способа передачи zeye в фрагментный шейдер, вычисляющий освещение. Дело в том, что в ряде случаев не получается построить фреймбуфер с приведенной на рис 5. структурой. Тогда можно использовать для хранения zeye обычную текстуру GL_RGBA8, "упаковав" в нее zeye.

Для этого сначала zeye приводится к отрезку [0,1], разделив ее на -zFar (в OpenGL zeye всегда принимает неположительные значения). После этого значение разбивается на четыре величины, для хранения каждой из которых достаточно 8 бит.

Шейдер, осуществляющий освещение по полученному "закодированному" RGBA-вектору восстанавливает значение, которое далее умножается на -zFar.

Ниже приводятся функции, осуществляющие подобное преобразование floating-point-значение из отрезка [0,1] в RGBA-формат и обратно.

//
// pack a floating point value from [0,1] into RGBA8 vector
//
vec4 packFloatToVec4i ( const float value )
{
    const vec4 bitSh   = vec4 ( 256.0*256.0*256.0, 256.0 * 256.0, 256.0, 1.0 );
    const vec4 bitMask = vec4 ( 0.0, vec3 ( 1.0 / 256.0 ) );
	
    vec4 res = fract ( value * bitSh );
	
    return res - res.xxyz * bitMask;
}

//
// unpack a [0,1] value from a RGBA8 vec4
//
float unpackFloatFromVec4i ( const vec4 value )
{
    const vec4 bitSh = vec4 ( 1.0 / (256.0*256.0*256.0), 1.0 / (256.0*256.0), 1.0 / 256.0, 1.0 );

    return dot ( value, bitSh );
}

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

Ниже приводятся фрагменты кода на С++, создающие текстуру со значениями глубины и копирующую значения глубины из z-буфера в нее.

//
// create depth texture
//
glGenTextures   ( 1, &depthMap );
glBindTexture   ( GL_TEXTURE_RECTANGLE_ARB, depthMap );
glTexParameteri ( GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
glTexParameteri ( GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
glTexParameteri ( GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
glTexParameteri ( GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_MIN_FILTER, GL_NEAREST );

glTexImage2D    ( GL_TEXTURE_RECTANGLE_ARB, 0, GL_DEPTH_COMPONENT, 512, 512, 0,
                   GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, NULL );

// 
// copy valus from z-buffer to it
//
glEnable           ( GL_TEXTURE_RECTANGLE_ARB );
glReadBuffer       ( GL_COLOR_ATTACHMENT0_EXT );
glActiveTextureARB ( GL_TEXTURE2_ARB );
glBindTexture      ( GL_TEXTURE_RECTANGLE_ARB, depthMap );
glCopyTexImage2D   ( GL_TEXTURE_RECTANGLE_ARB, 0, GL_DEPTH_COMPONENT, 0, 0, 512, 512, 0 );

Полностью исходный код содержится в примерах, доступных по ссылке в конце статьи.

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

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

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

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

Рис 6. Часть сферы, действительно требующая обработки.

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

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

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

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

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

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

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

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

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

Поэтому стоимость освещения определяется фактически только количеством реально освещаемых пикселов для каждого источника света.

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

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

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

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

Рис 7.Разбиения окна на прямоугольники для локализации небольших источников света.

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

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

При этом положения этих источников (равно как и их яркость и цвет) изменяются довольно редко и обычно нет никакой необходимости в отбрасывания этими источниками теней. Это позволяет достаточно эффективно использовать deferred shading для подобного имитирования глобальной освещенности.

В игре STALKER была также реализована еще одна интересная идея - кроме двух цветов в одном из свободных каналов G-буфера хранится номер (индекс) материала - materialId.

На этапе освещения materialId используется для выбора слоя из 3D-текстуры, определяющей свойства материала. Фактически основным свойством материала является то, как по двум скалярным произведениям - (n,l) и (n,h) - получить коэффициенты смешивания двух базовых цветов с учетом собственной светимости материала (в игре ряд объектов обладают собственной светимостью, например, глаза монстров).

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

За счет этого удалось прийти к очень небольшому числу материалов (не более 10), размер текстуры для индексирования по (n,l) был взят равным 16, для индексирования по (n,h) - равным 256.

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

В DX10.1-версии deferred shading'а в игре STALKER:Чисто небо была использована интересная оптимизация - использовался G-буфер, состоящий всего из двух текстур. В одной текстуре формата RGBA8 хранился диффузный цвет (в RGB компонентах) и собственная светимость em (в альфа-компоненте). Вторая текстура (формата RGBA_16F) в первой компоненте хранила zeye, во второй и третьей - nx и nн, в четвертую были упакованы сразу две величины - materialId и ambient occlusion.

Рис 8. Строение G-буфера в игре STALKER:Чистое Небо.

За счет использования подобного подхода удалось заметно сократить количество бит на один пиксел. Обратите внимание на то, что используемые текстуры имеют разное количество бит на пиксел - подобная возможность (использования при MRT текстур с разным числом бит на пиксел) появилась на GPU с 4-й шейдерной моделью (GeForce 8xxx).

Построенный G-буфер можно также использовать и для реализации ряда других эффектов, таких как слоистый туман, мягкие частицы, screen-space ambient ocllusion(SSAO) и др.

Алгоритм deferred shading кроме ряда плюсов обладает и некоторыми недостатками - он не поддерживает полупрозрачные объекты и стандартные средства антиалиасинга довольно плохо ложатся на его архитектуру.

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

Это может дать и некоторые плюсы - так при рендеринге воды, можно использовать значения z для дна, для точного расчета преломления.

На сайте Humus'а можно скачать альтернативный вариант работы с полупрозрачными объектами в deferred shading, только этот пример требует DX10 (а значит и убожество под названием m$ vi$ta).

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

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

//
// Deferred Shading pass 1 (transparent objects)
//

varying	vec3 pos;
varying	vec3 n;
varying	vec3 t;
varying	vec3 b;

uniform sampler2D	diffMap;
uniform sampler2D	bumpMap;

void main (void)
{
    vec3  nn    = 2.0*texture2D ( bumpMap, gl_TexCoord [0].xy ).xyz - vec3 ( 1.0 );
    float alpha = 0.6;
    float y     = gl_FragCoord.y * 0.5;
	
    if ( alpha < 1.0 ) 
        if ( fract ( y ) < 0.5 )
           discard; 
	
    nn = 0.5 * ( normalize ( nn.x * t + nn.y * b + nn.z * n ) + 1.0 );

    gl_FragData [0] = vec4 ( nn, 1.0 );
    gl_FragData [1] = vec4 ( texture2D ( diffMap, gl_TexCoord [0].xy ).rgb, alpha );
}

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

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

//
// Deferred Shading alpha-blending of transparent object
//
#extension GL_ARB_texture_rectangle: enable

#define EPS 0.01

uniform sampler2DRect	colorMap;
uniform sampler2DRect	diffMap;

void main (void)
{
    vec4  c  = texture2DRect ( colorMap, gl_FragCoord.xy );
    vec4  c2 = texture2DRect ( colorMap, gl_FragCoord.xy + vec2 ( 0.0, 1.0 ) );
    float a  = texture2DRect ( diffMap,  gl_FragCoord.xy ).a;
    float a2 = texture2DRect ( diffMap,  gl_FragCoord.xy + vec2 ( 0.0, 1.0 ) ).a;
	
    if ( a2 < 1.0 - EPS )          // we have semitransparent pixels
        c = c * (1.0 - a2) + c2 * a2;
    else
    if ( a < 1.0 - EPS )
        c = c * a + c2 * (1.0 - a );

    gl_FragColor = vec4 ( c );
}

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

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

transparent surface with deferred shading

transparent surface with deferred shading

Рис 9. Сцена с освещаемой полупрозрачной поверхностью.

В игре STALKER бы использован довольно простой способ реализации антиалиасинга для deferred shading. Основная идея этого способа заключается в том, что алиасинг наблюдается в тех местах, где в G-буфере есть разрывы (скачки). Поэтому если к G-буферу применить оператор edge detect, то он выделит те места, где возможен алиасинг. При этом edge detect обычно применяют только к значениям глубины и нормалям.

На следующем рисунке приводится сумма операторов edge detect для буферов с z-координатой и нормалью для тестовой сцены.

Рис 10. Результат применения операции edge-detect к zeye и нормалям.

Светлым местам на данном изображении соответствуют резки скачки глубины и/или нормали, т.е. именно те места, где и нужно осуществлять сглаживание. Тогда можно для текущего фрагмента найти суммарный edge detect по значениям глубины и нормали - w, а затем, если эта величина выше некоторого уровня, использовать ее для усреднения значения в окрестности точки, например как приведено в следующем фрагмента кода.

vec2 dx    = vec2 ( w, 0.0 );               // use w as a filter radius
vec2 dy    = vec2 ( 0.0, w );
vec4 final = (texture2D ( colorMap, tex + dx ) + texture2D ( colorMap, tex - dx ) + 
              texture2D ( colorMap, tex + dy ) + texture2D ( colorMap, tex - dy )) * 0.25;

Рис 11. Скриншоты с большим количеством источников света и антиалиасингом.

Кроме того существует еще два варианта рендеринга, очень похожих на deferred shading - это Light Pre-Pass Renderer и Light Indexed Deferred Rendering.

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

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

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

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

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

Классика по deferred shading'у - Deferred Shading in S.T.A.L.K.E.R..

Также в GPU Gems III тоже есть глава на эту тему Deferred Shading in Tabula Rasa.

Довольно полезный текст - Deferred Rendering in Killzone 2.

Еще одна статья (правда ориентированная на XNA) - Deferred Shading in XNA.

Очень неплохой рассказ о реализации deferred shading и использовании его для имитации глобального освещение Journal of Ysaneya.

Презентация Антона Капланяна с SIGGRAPH'2010 - CryENGINE 3: reaching the speed of light (pdf).

Compact Normal Storage for small G-Buffers

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

Используются технологии uCoz