Моделирование глубины резкости. Depth Of Field (DOF).

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

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

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

По законам оптики существует соотношение между расстоянием от линзы до "премника" и расстоянием от линзы до объекта, при котром объект будет в фокусе.

Рис 1. Работа простейшего объектива.

Это отношение задается следущей формулой, здесь через u и v обозначены расстояния от линзы до "приемника" и объекта, а через f - т.н. фокусноео расстояние объектива.

Если мы расположим поверхность на расстоянии d от объектива, то точек на "приемнике" будет соответствовать круг на этой плоскости - так нызваемый Circle Of Confusion (COC).

Существует формула для получения диаметра COC по параметрам объектива f, a и фокусному расстоянию dfocus:

Однако для применения на практике эта формула довольно сложна и мы длаее будем опираться на гораздо более простой подход, рассмотренный в книгах ShaderX2 и ShaderX3.

Свойства объектива мы будем характеризовать всего двумя величинами - фокусным расстоянием (focalDistance) и тем, как зависит степень размычтости объекта от отклонения расстояния до фокусного (focalRange).

Это позволяет прямо во фрагментном шейдере по z-координате (в системе координат наблюдателя) вычислить степень размытости для данного фрагмента (blur) и записать ее в альфа-канал получающегося изображения.

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

//
// Vertex shader of 1st pass of depth-of-field rendering
//

varying float z;

void main(void)
{
    vec4 pos = gl_ModelViewMatrix * gl_Vertex;

    z               = pos.z;
    gl_Position     = ftransform();
    gl_TexCoord [0] = gl_MultiTexCoord0;
}

//
// Fragment shader of 1st pass of depth-of-field rendering
//

varying float     z;
uniform sampler2D tex;
uniform float focalDistance, focalRange;

void main (void)
{
    float   blur = clamp ( abs ( focalDistance  + z ) / focalRange, 0.0, 1.0 );
    gl_FragData [0] = vec4 ( texture2D ( tex, gl_TexCoord [0].st).rgb, blur );

}

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

Рис 2. Использование билинейной интерполяции.

//
// Fragment shader of 2nd pass of depth-of-field rendering
//

uniform sampler2D tex;

void main (void)
{
    const vec2  d1 = vec2 ( 1.0/512.0, 1.0/512.0 );
    const vec2  d2 = vec2 ( 1.0/512.0, -1.0/512.0 );
    const vec2  d3 = vec2 ( -1.0/512.0, 1.0/512.0 );
    const vec2  d4 = vec2 ( -1.0/512.0, -1.0/512.0 );

    vec2    p = gl_TexCoord [0].st;
    
    gl_FragData [0] = (texture2D ( tex, vec2 ( p + d1 ) ) + 
                       texture2D ( tex, vec2 ( p + d2 ) ) + 
                       texture2D ( tex, vec2 ( p + d3 ) ) + 
                       texture2D ( tex, vec2 ( p + d4 ) ) ) * 0.25;
}

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

Простейший вариант заключается в степени размытия (blur) из альфа-канала исходного изображения для управления размером адра фильтра - радиус фильтра задается как blur*radiusScale:

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

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

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

Ниже приводится соответствующая прогрммная реализация.

//
// Fragment shader of 3rd pass of depth-of-field rendering
//

uniform sampler2D tex;
uniform sampler2D texLow;
uniform float     radiusScale;

void main (void)
{
    const vec2 poisson1 = vec2 (  0.0,       0.0      );
    const vec2 poisson2 = vec2 (  0.527837, -0.85868  );
    const vec2 poisson3 = vec2 ( -0.040088,  0.536087 );
    const vec2 poisson4 = vec2 ( -0.670445, -0.179949 );
    const vec2 poisson5 = vec2 ( -0.419418, -0.616039 );
    const vec2 poisson6 = vec2 (  0.440453, -0.639399 );
    const vec2 poisson7 = vec2 ( -0.757088,  0.349334 );
    const vec2 poisson8 = vec2 (  0.574619,  0.685879 );
    
    vec2    p  = gl_TexCoord [0].st;
    vec4    c  = texture2D ( tex, p );
    float   cd = c.a;
    float   discRadius    = cd * 10.0 / 512.0;
    float   discRadiusLow = discRadius * 0.4 * 4.0;
    vec4    tapLow, tapHigh, tap;
    float   blur;
    
    c = vec4 ( 0.0 );

                                            // sample 1
    tapLow   = texture2D ( texLow, p + poisson1 * discRadiusLow );
    tapHigh  = texture2D ( tex,    p + poisson1 * discRadiusLow );
    blur     = tapHigh.a;
    tap      = mix ( tapHigh, tapLow, blur );
        
                                            // apply leaking reduction
    tap.a  = ( tap.a >= cd ? 1.0 : tap.a );
    c.rgb += tap.rgb * tap.a;
    c.a   += tap.a;
    
                                            // sample 2
    tapLow   = texture2D ( texLow, p + poisson2 * discRadiusLow );
    tapHigh  = texture2D ( tex,    p + poisson2 * discRadiusLow );
    blur     = tapHigh.a;
    tap      = mix ( tapHigh, tapLow, blur );
        
                                            // apply leaking reduction
    tap.a  = ( tap.a >= cd ? 1.0 : tap.a );
    c.rgb += tap.rgb * tap.a;
    c.a   += tap.a;
    
                                            // sample 3
    tapLow   = texture2D ( texLow, p + poisson3 * discRadiusLow );
    tapHigh  = texture2D ( tex,    p + poisson3 * discRadiusLow );
    blur     = tapHigh.a;
    tap      = mix ( tapHigh, tapLow, blur );
        
                                            // apply leaking reduction
    tap.a  = ( tap.a >= cd ? 1.0 : tap.a );
    c.rgb += tap.rgb * tap.a;
    c.a   += tap.a;
    
                                            // sample 4
    tapLow   = texture2D ( texLow, p + poisson4 * discRadiusLow );
    tapHigh  = texture2D ( tex,    p + poisson4 * discRadiusLow );
    blur     = tapHigh.a;
    tap      = mix ( tapHigh, tapLow, blur );
        
                                            // apply leaking reduction
    tap.a  = ( tap.a >= cd ? 1.0 : tap.a );
    c.rgb += tap.rgb * tap.a;
    c.a   += tap.a;
    
                                            // sample 5
    tapLow   = texture2D ( texLow, p + poisson5 * discRadiusLow );
    tapHigh  = texture2D ( tex,    p + poisson5 * discRadiusLow );
    blur     = tapHigh.a;
    tap      = mix ( tapHigh, tapLow, blur );
        
                                            // apply leaking reduction
    tap.a  = ( tap.a >= cd ? 1.0 : tap.a );
    c.rgb += tap.rgb * tap.a;
    c.a   += tap.a;

                                            // sample 6
    tapLow   = texture2D ( texLow, p + poisson6 * discRadiusLow );
    tapHigh  = texture2D ( tex,    p + poisson6 * discRadiusLow );
    blur     = tapHigh.a;
    tap      = mix ( tapHigh, tapLow, blur );
        
                                            // apply leaking reduction
    tap.a  = ( tap.a >= cd ? 1.0 : tap.a );
    c.rgb += tap.rgb * tap.a;
    c.a   += tap.a;
    
                                            // sample 7
    tapLow   = texture2D ( texLow, p + poisson7 * discRadiusLow );
    tapHigh  = texture2D ( tex,    p + poisson7 * discRadiusLow );
    blur     = tapHigh.a;
    tap      = mix ( tapHigh, tapLow, blur );
        
                                            // apply leaking reduction
    tap.a  = ( tap.a >= cd ? 1.0 : tap.a );
    c.rgb += tap.rgb * tap.a;
    c.a   += tap.a;
    
                                            // sample 8
    tapLow   = texture2D ( texLow, p + poisson8 * discRadiusLow );
    tapHigh  = texture2D ( tex,    p + poisson8 * discRadiusLow );
    blur     = tapHigh.a;
    tap      = mix ( tapHigh, tapLow, blur );
        
                                            // apply leaking reduction
    tap.a  = ( tap.a >= cd ? 1.0 : tap.a );
    c.rgb += tap.rgb * tap.a;
    c.a   += tap.a;
    
    gl_FragData [0] = c / c.a;
}

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

//
// Example of depth-of-field
//
// Author: Alex V. Boreskoff <steps3d@narod.ru>,<steps3d@gmail.com>
//

#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
unsigned    decalMap;                   // decal (diffuse) texture
unsigned    stoneMap;
unsigned    teapotMap;
unsigned    blockMap;
unsigned    depthMap;

float   angle = 0;
float   yaw   = 0;
float   pitch = 0;
float   roll  = 0;
float   focalDistance = 4.5;
float   focalRange    = 20;
float   radiusScale   = 3.0 / 512.0;

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

FrameBuffer buffer  ( 512, 512, FrameBuffer :: depth32 );
FrameBuffer buffer2 ( 512/4, 512/4 );
FrameBuffer buffer3 ( 512/4, 512/4 );

GlslProgram program1;                       // build G-buffer
GlslProgram program2;                       // test G-buffer
GlslProgram program3;                       // test G-buffer
GlslProgram blur;

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

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

    drawBox  ( Vector3D ( -5, -5, 0 ),   Vector3D ( 10, 10, 3 ),   stoneMap, false );
    drawBox  ( Vector3D ( 3, 2, 0.5 ),   Vector3D ( 1,  2,  2 ),   decalMap,  true  );
    drawBox  ( Vector3D ( -3, -2, 0.5 ), Vector3D ( 1,  2,  2 ),   teapotMap, true  );
    drawBox  ( Vector3D ( 1, -1, 0.25 ), Vector3D ( 1,  2,  1.5 ), blockMap,  true  );
    drawBox  ( Vector3D ( -3, 3, 0.7 ),  Vector3D ( 1.5,  2,  1 ), decalMap,  true  );
    drawBox  ( Vector3D ( 3, -3, 0.25 ), Vector3D ( 1,  1,  1.5 ), blockMap,  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 ()
{
    buffer.bind   ();
    program1.bind ();
    
    glClear ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

    program1.bind            ();
    program1.setUniformFloat ( "focalDistance", focalDistance );
    program1.setUniformFloat ( "focalRange",    focalRange    );
    camera.apply             ();
    
    displayBoxes ();
    
    program1.unbind ();
    buffer.unbind   ( true );
    
    glBindTexture ( GL_TEXTURE_2D, buffer.getColorBuffer () );
    
    program2.bind   ();
    buffer2.bind    ();
    startOrtho      ( 512/4, 512/4 );
    drawQuad        ( 512/4, 512/4 );
    endOrtho        ();
    buffer2.unbind  ( true );
    program2.unbind ();
    
    glBindTexture ( GL_TEXTURE_2D, buffer2.getColorBuffer () );
    
    blur.bind       ();
    buffer3.bind    ();
    startOrtho      ( 512/4, 512/4 );
    drawQuad        ( 512/4, 512/4 );
    endOrtho        ();
    buffer3.unbind  ( true );
    blur.unbind    ();
    
    glActiveTextureARB ( GL_TEXTURE1_ARB );
    glBindTexture      ( GL_TEXTURE_2D, buffer2.getColorBuffer () );
    
    glActiveTextureARB ( GL_TEXTURE0_ARB );
    glBindTexture      ( GL_TEXTURE_2D, buffer.getColorBuffer () );
    
    program3.bind   ();
    program3.setUniformFloat ( "radiusScale", radiusScale );
    startOrtho      ( 512, 512 );
    drawQuad        ( 512, 512 );
    endOrtho        ();
    program3.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 == '+' )
        focalDistance += 0.1;
    else
    if ( key == '-' )
        focalDistance -= 0.1;
    else
    if ( key == '*' )
        focalRange += 0.3;
    else
    if ( key == '/' )
        focalRange -= 0.3;

    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 Depth Of Field Demo" );

                                // 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/water.bmp" );
    blockMap  = createTexture2D ( true, "../../Textures/block.bmp" );
    
    buffer.create ();
    buffer.bind   ();
    
    if ( !buffer.attachColorTexture ( GL_TEXTURE_2D, buffer.createColorTexture ( GL_RGBA, GL_RGBA8 ), 0 ) )
        printf ( "buffer error with color attachment\n");

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

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

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

    buffer3.create ();
    buffer3.bind   ();
    
    if ( !buffer3.attachColorTexture ( GL_TEXTURE_RECTANGLE_ARB, buffer3.createColorRectTexture ( GL_RGBA, GL_RGBA8 ), 0 ) )
        printf ( "buffer3 error with color attachment\n");

    if ( !buffer3.isOk () )
        printf ( "Error with framebuffer3\n" );
                    
    buffer3.unbind ();

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

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

        return 3;
    }

    if ( !program3.loadShaders ( "dof1-p3.vsh", "dof1-p3.fsh" ) )
    {
        printf ( "Error loading shaders3:\n%s\n", program3.getLog ().c_str () );

        return 3;
    }

    if ( !blur.loadShaders ( "blur.vsh", "blur.fsh" ) )
    {
        printf ( "Error loading shaders3:\n%s\n", program3.getLog ().c_str () );

        return 3;
    }

    program1.bind        ();
    program1.setTexture  ( "tex", 0 );
    program1.unbind      ();

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

    program3.bind        ();
    program3.setTexture  ( "tex",    0 );
    program3.setTexture  ( "texLow", 1 );
    program3.unbind      ();
    
    blur.bind        ();
    blur.setTexture  ( "tex", 0 );
    blur.unbind      ();

    camera.setRightHanded ( false );
    
    printf ( "Depth of Field demo.\n\tUse + and - to change focal distance.\n\tUse * and / to change focal range.\n\tUse mouse and wsad to control camera.\n" );
    
    glutMainLoop ();

    return 0;
}

depth of field screenshot

depth of field screenshot

Рис 3. Скриншоты программы.

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

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