steps3D - Tutorials - Variance Shadow Maps (VSM)

Variance Shadow Maps (VSM)

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

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

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

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

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

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

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

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

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

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

shadow map reading equation

В силу нелинейности данной операции, ее нельзя менять местами с традиционными операциями фильтрования (которые все являются линейными), т.е. если у нас есть операция фильтрования F, то в общем случае S(F(p)) != F (S(p)).

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

В данном случае нужно подвергать фильтрации не саму теневую карту, а результат применения операции чтения из нее (S).

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

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

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

Более подробно об этом подходе можно прочитать в "GPU Gems 2", где этому посвящена целая глава, написанная Юрием Уральским. Пример из этой книги входит в последний NVSDK, так что можно посмотреть на исходный код.

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

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

Поэтому вместо обычной карты глубины берется текстура с несколькими каналами, в которую при помощи соответствующего шейдера заносятся как само значение глубины z, так и значение квадрата глубины z2 (под глубиной понимается приведенная к отрезку [0,1] значение глубины).

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

По этим моментам легко можно найти среднее значение для интересующего нас распределения глубин в окрестности точки и среднеквадратическое отклонение:

computing mean and average quadratic

Тогда можно воспользоваться следующим утверждением (неравенством Чебышева):

Пусть z - случайная величина со средним значением mu и среднеквадратичным отклонением sigma squared.

Тогда для t >mu справедливо

Chebyshev inequality

Если внимательно посмотреть на P(z > t), то можно заметить, что это именно то, что нам нужно - доля значений из окрестности точки, для которой значение глубины больше чем t, т.е. величина полутени.

В результате мы приходим к использованию следующей функции для определения степени затенения:

shadow computation for VSM

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

void main (void)
{
    float z = gl_FragCoord.z;

    gl_FragColor = vec4 ( z, z*z, z, 1.0 );
}

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

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

uniform sampler2D vsmMap;
uniform sampler2D mainMap;

void main (void)
{
    vec4	clr  = texture2D ( mainMap, gl_TexCoord [0].xy );
    vec4	tex  = gl_TexCoord [1] / gl_TexCoord [1].w;
    vec4	vsm  = texture2D ( vsmMap, tex.xy );
    float	mu   = vsm.x;
    float	s2   = vsm.y - mu*mu;
    float	pmax = s2 / ( s2 + (tex.z - mu)*(tex.z - mu) );

    gl_FragColor = clr;

    if ( tex.z >= vsm.x )
        gl_FragColor = vec4 ( vec3 ( pmax ), 1.0 ) * clr;
}

На следующих двух рисунках приводится примеры работы этого метода.

VSM shadows screenshot

VSM shadows screenshot

Рис 1. Примеры теней построенных при помощи VSM.

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

Авторы метода утверждают, что у них все хорошо работает для 16-битовых float-ов, однако у меня приемлемые результаты получились лишь при использовании нормальных 32-битовых float-ов.

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

Подобные артефакты можно найти на следующем рисунке.

VSM shadows artecfacts screenshot

Рис 2. Пример артефактов.

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

Ниже приводится фрагменты кода, используемые для инициализации FBO и собственно самого рендеринга.

FrameBuffer	buffer  ( shadowMapSize, shadowMapSize, FrameBuffer :: depth32 );
FrameBuffer	buffer2 ( shadowMapSize, shadowMapSize, FrameBuffer :: depth32 );
GlslProgram	writeProgram;
GlslProgram	xBlur;
GlslProgram yBlur;
GlslProgram	vsm;

float   mv [16];                        // to hold modelview matrix used when rendering from light
float   pr [16];                        // to hold projection matrix used when rendering from light

void	renderToShadowMap ()
{
    buffer.bind ();
    writeProgram.bind ();

    glDisable ( GL_TEXTURE_2D );

    glEnable        ( GL_POLYGON_OFFSET_FILL );
    glPolygonOffset ( 4, 4 );

                                                    // setup projection
    glViewport ( 0, 0, shadowMapSize, shadowMapSize );
    glClear    ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

    glMatrixMode   ( GL_PROJECTION );
    glLoadIdentity ();

    gluPerspective ( 120.0, 1, 0.1, 60.0 );
  	gluLookAt      ( light.x, light.y, light.z,     // eye
                     center.x, center.x, center.z,  // center
                     0, 0, 1 );                     // up

    glMatrixMode   ( GL_MODELVIEW );
    glLoadIdentity ();

                                                    // get modelview and projections matrices
    glGetFloatv ( GL_MODELVIEW_MATRIX,  mv );
    glGetFloatv ( GL_PROJECTION_MATRIX, pr );

                                                    // now render scene from light position
    renderScene ();
                                                    // restore state
    glDisable        ( GL_POLYGON_OFFSET_FILL );
    glEnable         ( GL_TEXTURE_2D );

    writeProgram.unbind ();
    buffer.unbind ();
}

void	drawQuad ( unsigned tex )
{
    glActiveTextureARB ( GL_TEXTURE0_ARB );
    glColor4f     ( 1, 1, 1, 1 );
    glBindTexture ( GL_TEXTURE_2D, tex );
    glEnable      ( GL_TEXTURE_2D );
    glBegin       ( GL_QUADS );

        glTexCoord2f ( 0, 0 );
        glVertex2f   ( 0, 0 );

        glTexCoord2f ( 1,   0 );
        glVertex2f   ( shadowMapSize, 0 );

        glTexCoord2f ( 1, 1 );
        glVertex2f   ( shadowMapSize, shadowMapSize );

        glTexCoord2f ( 0, 1 );
        glVertex2f   ( 0, shadowMapSize );

    glEnd   ();
}

void    startOrtho ()
{
    glMatrixMode   ( GL_PROJECTION );                   // select the projection matrix
    glPushMatrix   ();                                  // store the projection matrix
    glLoadIdentity ();                                  // reset the projection matrix
                                                        // set up an ortho screen
    glOrtho        ( 0, shadowMapSize, 0, shadowMapSize, -1, 1 );
    glMatrixMode   ( GL_MODELVIEW );                    // select the modelview matrix
    glPushMatrix   ();                                  // store the modelview matrix
    glLoadIdentity ();                                  // reset the modelview matrix

    glDisable   ( GL_DEPTH_TEST );
    glDepthMask ( GL_FALSE );
}

void    endOrtho ()
{
    glMatrixMode ( GL_PROJECTION );                     // select the projection matrix
    glPopMatrix  ();                                    // restore the old projection matrix
    glMatrixMode ( GL_MODELVIEW );                      // select the modelview matrix
    glPopMatrix  ();                                    // restore the old projection matrix

    glEnable    ( GL_DEPTH_TEST );
    glDepthMask ( GL_TRUE );
}

void	blurMap ()
{
    startOrtho ();

    buffer2.bind ();
    xBlur  .bind ();                                    // perform x-blurring first

    drawQuad ( buffer.getColorBuffer () );

    xBlur.unbind   ();
    buffer2.unbind ();

    buffer.bind ();
    yBlur.bind ();

    drawQuad ( buffer2.getColorBuffer () );

    yBlur.unbind ();
    buffer.unbind ();

    endOrtho ();
}

void	renderScene ()
{
    glClear ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

    glMatrixMode       ( GL_MODELVIEW );
    glColor3f          ( 0.5, 0.5, 0.5 );
    glActiveTextureARB ( GL_TEXTURE0_ARB );
    glEnable           ( GL_TEXTURE_2D );
    glBindTexture      ( GL_TEXTURE_2D, diffuseMap );

    glMatrixMode    ( GL_MODELVIEW );
    glPushMatrix    ();
    glRotatef       ( 90, 1, 0, 0 );
    glTranslatef    ( 0, 0.5, 0 );
    glutSolidTeapot ( 2.5 );
    glPopMatrix     ();
                                                    // draw unlit floor
    glBindTexture      ( GL_TEXTURE_2D, ambMap );

    glBegin ( GL_QUADS );

    glTexCoord2f ( 0, 0 );
    glVertex3f   ( -7, -7, -1 );

    glTexCoord2f ( 1, 0 );
    glVertex3f   ( 7, -7, -1 );

    glTexCoord2f ( 1, 1 );
    glVertex3f   ( 7, 7, -1 );

    glTexCoord2f ( 0, 1 );
    glVertex3f   ( -7, 7, -1 );

    glEnd   ();
}

void display ()
{
    renderToShadowMap ();                       // compute shadow map

    blurMap ();

    buffer.buildMipmaps ();
                                                // clear buffers
    glClear ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

    reshape ( width, height );                  // setup modelview and projection

    glMatrixMode   ( GL_MODELVIEW );
    glPushMatrix   ();

    glRotatef    ( rot.x, 1, 0, 0 );
    glRotatef    ( rot.y, 0, 1, 0 );
    glRotatef    ( rot.z, 0, 0, 1 );

												// setup shadowing
    glActiveTextureARB ( GL_TEXTURE1_ARB );
    glBindTexture      ( GL_TEXTURE_2D, buffer.getColorBuffer () );
    glColor4f          ( 1, 1, 1, 1 );
                                                // set correct texcoord transform
    glMatrixMode  ( GL_TEXTURE );

    glLoadIdentity ();
    glTranslatef   ( 0.5, 0.5, 0.5 );           // remap from [-1,1]^2 to [0,1]^2
    glScalef       ( 0.5, 0.5, 0.5 );
    glMultMatrixf  ( pr );
    glMultMatrixf  ( mv );

    glActiveTextureARB ( GL_TEXTURE0_ARB );

    vsm.bind    ();
    renderScene ();
    vsm.unbind  ();

    glActiveTextureARB ( GL_TEXTURE1_ARB );
    glDisable          ( GL_TEXTURE_2D   );
    glActiveTextureARB ( GL_TEXTURE0_ARB );

                                                 // draw the light
    glMatrixMode ( GL_MODELVIEW );
    glPopMatrix  ();
    glPushMatrix ();

    glTranslatef       ( light.x, light.y, light.z );
    glActiveTextureARB ( GL_TEXTURE0_ARB );
    glDisable          ( GL_TEXTURE_2D );
    glutSolidSphere    ( 0.1f, 15, 15 );
    glPopMatrix        ();

    glMatrixMode ( GL_TEXTURE );
    glPopMatrix  ();

    glMatrixMode ( GL_MODELVIEW );

    glutSwapBuffers ();
}

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


                                // create window
    glutCreateWindow ( "OpenGL Variance Shadow Maps demo" );

                                // register handlers
    glutDisplayFunc  ( display );
    glutReshapeFunc  ( reshape );
    glutKeyboardFunc ( key     );
    glutMouseFunc    ( mouse   );
    glutMotionFunc   ( motion  );
    glutIdleFunc     ( animate );

    init           ();
    initExtensions ();

    assertExtensionsSupported ( "GL_ARB_shadow GL_ARB_depth_texture EXT_framebuffer_object" );

    ambMap      = createTexture2D ( true,  "../../Textures/oak.bmp" );
    diffuseMap  = createTexture2D ( true,  "../../Textures/Oxidated.jpg" );

    if ( isExtensionSupported ( "GL_ARB_texture_float" ) )
        format = GL_RGBA32F_ARB;
    else
    if ( isExtensionSupported ( "GL_NV_float_buffer" ) )
        format = GL_FLOAT_RGBA32_NV;
    else
    if ( isExtensionSupported ( "GL_ATI_texture_float" ) )
        format = GL_RGBA_FLOAT32_ATI;
    else
    {
        printf ( "Floating-point textures not supported !\n" );

        return 1;
    }

    unsigned screenMap = buffer.createColorTexture ( GL_RGBA, format , GL_CLAMP, FrameBuffer :: filterNearest );

    buffer.create ();
    buffer.bind   ();
    if ( !buffer.attachColorTexture ( GL_TEXTURE_2D, screenMap ) )
        printf ( "buffer error with color attachment\n");

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

    buffer.unbind ();

    unsigned screenMap2 = buffer2.createColorTexture ( GL_RGBA, format, GL_CLAMP, FrameBuffer :: filterNearest );

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

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

    buffer2.unbind ();

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

        return 3;
    }

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

        return 3;
    }

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

        return 3;
    }
	
    if ( !vsm.loadShaders ( "vsm.vsh", "vsm.fsh" ) )
    {
        printf ( "Error loading VSM shaders:\n%s\n", vsm.getLog ().c_str () );

        return 3;
    }

    xBlur.bind       ();
    xBlur.setTexture ( "mainTex", 0 );
    xBlur.unbind     ();
    yBlur.bind       ();
    yBlur.setTexture ( "mainTex", 0 );
    yBlur.unbind     ();

    vsm.bind       ();
    vsm.setTexture ( "mainMap", 0 );
    vsm.setTexture ( "vsmMap",  1 );
    vsm.unbind     ();

    glBindTexture ( GL_TEXTURE_2D, 0 );
	
    glutMainLoop ();

    return 0;
}

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

Исходную статью и пример от авторов метода можно найти здесь.

Valid HTML 4.01 Transitional

Напиши мне