Главная Статьи Ссылки Скачать Скриншоты Юмор Почитать Tools Проекты Обо мне Гостевая |
Практически все тени, встречаемые нами в реальной жизни являются мягкими (soft), т.е. изменение освещенности при переходе от полностью освещенного места к месту, находящимся в тени, носит непрерывный (а зачастую и гладкий, т.е. дифференцируемый) характер.
К сожалению классические методы, используемые для построения теней в OpenGL - теневые объемы и теневые карты - ориентированны именно на получение резких теней (без размытия, переход из тени в свет осуществляется скачком).
Иногда это ограничение пытаются обойти, используя вместо одного источника света, несколько близкорасположенных и строя тени отдельно для каждого из них.
Однако такой способ оказывается крайне дорогостоящим и при этом получаемые тени все равно не являются мягкими - в них четко отслеживается скачкообразное изменение интенсивности, соответствующее границе тени от одного из источников света.
Совсем недавно был предложен довольно интересный метод - Variance Shadow Maps - позволяющий получать мягкие тени путем некоторой модификации традиционного методы теневых карт.
Перед непосредственным рассмотрением данного метода давайте выясним с чем именно связаны проблемы метода теневых карт.
Фактически все они являются проявлением aliasing'а (подробнее о том, что это такое можно прочесть в этой статье). Из этого следует, что бороться с ними надо путем фильтрации теневой карты.
Однако тут нас ожидает неприятный сюрприз - дело в том, что механизм фильтрации, поддерживаемый OpenGL (пирамидальная, билинейная, трилинейная, анизотропная) не применима непосредственно к теневым картам (хотя они отлично работают для обычных текстур).
Причина этого заключается в том, что хотя теневые карты хранят значения глубины (т.е. расстояние до источника света), но сама операция чтения из теневой карты носит нелинейный (и даже разрывный) характер - происходит сравнение значения глубины из теневой карты с третьей текстурной координатой и в зависимости от результата этого сравнения возвращается либо ноль либо единица.
Фактически операцию чтения из теневой карты можно формально записать следующим образом:
В силу нелинейности данной операции, ее нельзя менять местами с традиционными операциями фильтрования (которые все являются линейными), т.е. если у нас есть операция фильтрования 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.
По этим моментам легко можно найти среднее значение для интересующего нас распределения глубин в окрестности точки и среднеквадратическое отклонение:
Тогда можно воспользоваться следующим утверждением (неравенством Чебышева):
Пусть z - случайная величина со средним значением и среднеквадратичным отклонением .
Тогда для t > справедливо
Если внимательно посмотреть на P(z > t), то можно заметить, что это именно то, что нам нужно - доля значений из окрестности точки, для которой значение глубины больше чем t, т.е. величина полутени.
В результате мы приходим к использованию следующей функции для определения степени затенения:
Данный подход можно легко реализовать при помощи шейдеров. Как и в обычном методе теневых карт, сначала осуществляется рендеринг из положения источника света. Однако при это вместо теневой карты используется обычная текстура, в которую шейдер записывает значение глубины и квадрата глубины. Ниже приводится пример такого шейдера.
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; }
На следующих двух рисунках приводится примеры работы этого метода.
Рис 1. Примеры теней построенных при помощи VSM.
К сожалению данный метод при всей своей красоте обладает и рядом недостатков. Наиболее серьезным является то, что вычисление среднеквадратического отклонения сильно чуствительно к ошибкам округления и поэтому для хранения значений глубины и квадрата глубины приходится использовать текстуры с 16- или 32-битовыми float-значениями.
Авторы метода утверждают, что у них все хорошо работает для 16-битовых float-ов, однако у меня приемлемые результаты получились лишь при использовании нормальных 32-битовых float-ов.
Кроме того, метод может давать и "ложные" светлые места там, где должна быть тень - дело в том, что для больших значений среднеквадратического отклонения мы получаем ненулевую освещенность.
Подобные артефакты можно найти на следующем рисунке.
Рис 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.
Исходную статью и пример от авторов метода можно найти здесь.