Light PrePass Rendering

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

При использовании этого метода все сцена выводится в так называемый G-буфер. В результате этого для каждого пиксела экрана в G-буфере хранится вся информация, необходимая для расчета освещенности данного пиксела. В число таких атрибутов обычно входят 3D-координаты, соответствующие данному пикселу, вектор нормали, цвет, светимость и т.п. На следующей таблице приводится структура G-буфера, использованная в игре STALKER: Shadow Of Chernobyl. Там G-буфер состоит из трех текстур RGBA_16F.

Таблица 1. Строение G-буфера в игре STALKER: Shadow Of Chernobyl.

Номер Компоненты Комментарий
0 RGB Единичный вектор нормали в точке (nx,ny,nz)
0 A Ambient occlusion hemi
1 RGB Координаты исходной точки (x,y,z)
1 A Индекс материала
2 RGB Диффузный цвет (dred,dgreen,dblue)
2 A Интенсивность бликов gloss

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

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

Однако у этого метода есть и свои недостатки. К ним в первую очередь относится высокие требования на пропускную способность памяти из-за большого объема памяти (bandwidth), выделяемого под G-буфер. Также структура G-буфера накладывает некоторые ограничения на используемые модели освещения. Кроме того, довольно сложно использовать MSAA (в отличии от традиционного, forward рендеринга).

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

Подобный подход был использован в таких играх, как Resistance 2, Uncharted, Blur и ряде других.

В простейшем варианте LPP Rendering работает следующим образом:

Рассмотрим теперь каким образом можно реализовать данный подход. В качестве G-буфера достаточно взять одну текстуру формата RGBA_16F - первые три компоненты можно использовать для хранения вектора нормали, а в альфа-компоненте - хранить zeye.

Ниже приводится вершинный шейдер для первого прохода.

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

void main(void)
{
    pos = vec3      ( gl_ModelViewMatrix * gl_Vertex );             // transformed point to world space
    n   = normalize ( gl_NormalMatrix * gl_Normal );                // transformed n
    t   = normalize ( gl_NormalMatrix * gl_MultiTexCoord1.xyz );
    b   = normalize ( gl_NormalMatrix * gl_MultiTexCoord2.xyz );

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

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

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

uniform sampler2D   bumpMap;

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

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

В этих уравнениях через n обозначена нормаль в точек, через li - единичный вектор из точки к i-му источнику света. Через IRGB,i обозначен цвет и яркость i-го источника света. atti обозначает зависимость освещенности от расстояния до i-го источника света. Через lum обозначена функция возвращающая яркость для заданного RGB-значения.

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

#extension GL_ARB_texture_rectangle: enable

varying vec3          pos;
uniform vec3          lightPos;
uniform vec3          lightColor;
uniform sampler2DRect dataMap;

void main (void)
{
    vec4    nz = texture2DRect ( dataMap, gl_FragCoord.xy );
    vec3    n  = nz.xyz;
    float   z  = nz.a;
    vec3    pp = pos * z / pos.z;
    vec3    l  = normalize ( lightPos - pp );
    vec3    v  = normalize ( -pp );
    vec3    h  = normalize ( l + v );
    float   diff = max ( 0.1, dot ( l, n ) );
    float   lum  = dot ( lightColor, vec3 ( 0.3, 0.59, 0.11 ) );
    float   spec = lum * pow ( max ( 0.0, dot ( h, n ) ), 40.0 );
    
    gl_FragColor = vec4 ( diff * lightColor.rgb, spec );
}

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

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

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

//
// LPP pass 3, apply lighting G-buffer
//

#define EPS 0.001

uniform sampler2D     decalMap;
uniform sampler2DRect lightMap;

void main (void)
{
    vec4    c     = texture2D     ( decalMap, gl_TexCoord [0].xy );
    vec4    l     = texture2DRect ( lightMap, gl_FragCoord.xy );
    vec3    diff  = l.rgb;
    vec3    chrom = diff / ( EPS + dot ( diff, vec3 ( 0.3, 0.59, 0.11 ) ) );
    vec3    spec  = chrom * l.a;

    gl_FragColor = vec4 ( diff * c.xyz + 0.6 * spec, 1.0 );
}

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

Рис 1. Простейший случай использования LPP.

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

#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"
#include    "fpTexture.h"

Vector3D    eye   ( -0.5, -0.5, 1.5 );      // camera position
Vector3D    light ( -0.5, -0.5, 1.5 );      // light position
Vector3D    lightColor ( 1, 1, 1 );         // light color
unsigned    stoneMap, woodMap, teapotMap, decalMap;
unsigned    bumpMap, bumpMap2, bumpMap3;
GLenum      format;

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 program1;                       // build G-buffer
GlslProgram program2;                       // build light buffer
GlslProgram program3;                       // render scene applying lighting

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 };
    
                                                // build RGBA_16F buffer with normal and z
    buffer.bind ();

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

    glClearColor ( 0, 0, 0, 0 );
    glClear      ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

    camera.apply ();
    displayBoxes ();

    program1.unbind ();
    buffer.unbind   ();
    
                        // now we have buffer with data  normal and z
                        // build light buffer
    Vector3D    p [4];
    camera.getPlanePolyForZ ( 1, p );
    
    glClear            ( GL_COLOR_BUFFER_BIT );
    glDisable          ( GL_DEPTH_TEST );
    glDepthMask        ( GL_FALSE );
    glActiveTextureARB ( GL_TEXTURE0_ARB );
    glBindTexture      ( GL_TEXTURE_RECTANGLE_ARB, buffer.getColorBuffer ( 0 ) ); 
    
    
                        // render lights
    buffer2.bind  ();
    program2.bind ();
    program2.setUniformVector ( "lightPos", camera.mapFromWorld ( light ) );
    program2.setUniformVector ( "lightColor", lightColor );
    
    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 ();
    buffer2.unbind  ();
    
    glEnable    ( GL_DEPTH_TEST );
    glDepthMask ( GL_TRUE );
    
                        // now render geometry and apply lighting buffer
    reshape ( buffer.getWidth (), buffer.getHeight () );
    
    glActiveTextureARB ( GL_TEXTURE2_ARB );
    glBindTexture      ( GL_TEXTURE_RECTANGLE_ARB, buffer2.getColorBuffer ( 0 ) ); 
    glActiveTextureARB ( GL_TEXTURE0_ARB );
    
    program3.bind ();

    glClear ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
    
    camera.apply ();
    
    displayBoxes ();

    program3.unbind ();
    glActiveTextureARB ( GL_TEXTURE2_ARB );
    glBindTexture      ( GL_TEXTURE_RECTANGLE_ARB, 0 ); 

    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 Light PrePass Rendering" );

                                // 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 ( (format = fpRgbaFormatWithPrecision ( 16 ) ) == 0 )
    {
        printf ( "Floating-point textures not supported !\n" );
        
        return 1;
    }
    
    unsigned screenMap0 = buffer.createColorRectTexture  ( GL_RGBA, format );
    
    buffer.create ();
    buffer.bind   ();
    
    if ( !buffer.attachColorTexture ( GL_TEXTURE_RECTANGLE_ARB, screenMap0, 0 ) )
        printf ( "buffer error with color attachment\n");

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

    unsigned screenMap1 = buffer2.createColorRectTexture ( GL_RGBA, format );
    
    buffer2.create ();
    buffer2.bind   ();
    
    if ( !buffer2.attachColorTexture ( GL_TEXTURE_RECTANGLE_ARB, screenMap1, 0 ) )
        printf ( "buffer2 error with color attachment\n");

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

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

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

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

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

    program2.bind ();
    program2.setTexture  ( "dataMap",  0 );
    program2.unbind      ();

    program3.bind ();
    program3.setTexture  ( "decalMap",  0 );
    program3.setTexture  ( "lightMap",  2 );
    program3.unbind      ();

    camera.setRightHanded ( false );
    
    glutMainLoop ();

    return 0;
}

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

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

#define NUM_LIGHTS  1000
#define MAX_DIST    1.5

Vector3D    light      [NUM_LIGHTS];        // light positions
Vector3D    lightStart [NUM_LIGHTS];        // light start positions
Vector3D    lightColor [NUM_LIGHTS];        // light colors

.
.
.

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

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

void    initLights ()
{
    for ( int i = 0; i < NUM_LIGHTS; i++ )
    {
        lightStart [i] = Vector3D ( rnd ( -4.5, 4.5 ), rnd ( -4.5, 4.5 ), rnd ( 0.3, 2.7 ) - 1.5 );
        lightColor [i] = Vector3D ( rnd ( 0.5, 1 ),    rnd ( 0.5, 1    ), rnd ( 0.5, 1 ) );
    }
}

void    animateLights ( float t )
{
    float   ca = cos ( 0.1  * t );
    float   sa = sin ( 0.13 * t );
    
    for ( int i = 0; i < NUM_LIGHTS; i++ )
    {
        float   x = lightStart [i].x;
        float   y = lightStart [i].y;
        float   z = lightStart [i].z; 
        
        light [i].x = ca * x - sa * y;
        light [i].y = sa * x + ca * y;
        light [i].z = 1.5 + z;
    }
}

void display ()
{
    GLenum  buffers [] = { GL_COLOR_ATTACHMENT0_EXT };
  
    buffer.bind ();     // build RGBA_16F buffer with normal and z

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

    glClearColor ( 0, 0, 0, 0 );
    glClear      ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

    camera.apply ();
    displayBoxes ();

    program1.unbind ();
    buffer.unbind   ();
    
                        // now we have buffer with data  normal and z
                        // build light buffer
    glDisable          ( GL_DEPTH_TEST );
    glDepthMask        ( GL_FALSE );
    glActiveTextureARB ( GL_TEXTURE0_ARB );
    glBindTexture      ( GL_TEXTURE_RECTANGLE_ARB, buffer.getColorBuffer ( 0 ) ); 
    
                        // render lights
    buffer2.bind  ();
    program2.bind ();
    
    glClear     ( GL_COLOR_BUFFER_BIT );
    glEnable    ( GL_BLEND );
    glBlendFunc ( GL_ONE, GL_ONE );
    
    for ( int i = 0; i < NUM_LIGHTS; i++ )
    {
        glMultiTexCoord3fv ( GL_TEXTURE1_ARB, light      [i] );
        glMultiTexCoord3fv ( GL_TEXTURE2_ARB, lightColor [i] );

        drawBoxCull ( light [i], Vector3D ( MAX_DIST ), false );
    }
    
    program2.unbind ();
    buffer2.unbind  ();
    
    glDisable   ( GL_BLEND );
    glEnable    ( GL_DEPTH_TEST );
    glDepthMask ( GL_TRUE );
    
                        // now render geometry and apply lighting buffer
    reshape ( buffer.getWidth (), buffer.getHeight () );
    
    glActiveTextureARB ( GL_TEXTURE2_ARB );
    glBindTexture      ( GL_TEXTURE_RECTANGLE_ARB, buffer2.getColorBuffer ( 0 ) ); 
    glActiveTextureARB ( GL_TEXTURE0_ARB );
    
    program3.bind ();

    glClear ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
    
    camera.apply ();
    
    displayBoxes ();

    program3.unbind ();
    glActiveTextureARB ( GL_TEXTURE2_ARB );
    glBindTexture      ( GL_TEXTURE_RECTANGLE_ARB, 0 ); 

    glutSwapBuffers ();
}

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

#extension GL_ARB_texture_rectangle: enable

varying vec3          pos;
uniform sampler2DRect dataMap;

#define MAX_DIST    1.5

float   atten ( float d )                               // compute distance attenuation factor, 0 -> 1, MAX_DIST -> 0
{
    const   float   offs0 = 1.0;
    const   float   offs1 = 1.0 / (1.0 + MAX_DIST);
    const   float   scale = 0.5 / (offs0 - offs1);
    
    return scale * ( 1.0 / (1.0 + d) - offs1 );
}

void main (void)
{
    vec3    lightPos   = gl_TexCoord [1].xyz / gl_TexCoord [1].w;
    vec3    lightColor = gl_TexCoord [2].xyz;

    vec4    nz = texture2DRect ( dataMap, gl_FragCoord.xy );
    vec3    n  = nz.xyz;
    vec3    pp = pos * nz.w / pos.z;
    float   d  = length    ( lightPos - pp );
    
    if ( d > MAX_DIST )                                 // too far away
        discard;
    
    vec3    l    = normalize ( lightPos - pp );
    vec3    v    = normalize ( -pp );
    vec3    h    = normalize ( l + v );
    float   at   = atten     ( d );
    float   lum  = dot ( lightColor, vec3 ( 0.3, 0.59, 0.11 ) );
    float   diff = at * max ( 0.1, dot ( l, n ) );
    float   spec = diff * lum * pow ( max ( 0.001, dot ( h, n ) ), 40.0 );
    
    gl_FragColor = vec4 ( diff * lightColor.rgb, spec );
}

Рис 2. Сцена с 1000 источников света.

Несколько полезных ссылок:

Презентация Вольфганга Энгеля про LPP.

Обсуждение LPP в блоге Вольфганга Энгеля

Еще одна реализация LPP, использующая в качестве G-буфера обычную текстуру RGB8.

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