Основы создания мягких теней (GLSL)
Автор: Сергей Резник
Данная статья является естественным продолжением и дополнением статьи «Реализация карт теней с использованием GLSL шейдеров» Дмитрия Беспалова (aka Executor).
Часто разработчикам хочется использовать в сцене так называемые «мягкие» тени, то есть такие, которые бы не имели артефактов техники shadow maps — зазубрин на краях тени.
Введение
1. Несколько выборок, вместо одной
2. Forward-render хорошо, а post-process-то лучше :)
Введение
Чтобы лучше понять, о чем идет речь, и с чем мы будем бороться, посмотрим на картинку:
Вследствие конечного (обычно небольшого) размера карты теней появляются так называемые погрешности дискретизации, которые вызывают появление ступенчатости теней.
В данной статье мы попытаемся найти способ устранить ступенчатость тени и придать им более гладкий и приятный вид. Существует много вариантов придания гладкости теням, но я бы хотел остановиться всего на двух. Эти методы являются простейшими, но в тоже время дают неплохой результат, и не очень влияют на скорость отрисовки. Для создания мягких теней, нам потребуется немного больше данных, нежели в стандартной технике, описанной в предыдущей статье.
Что нам понадобиться кроме текстуры с глубиной сцены: всего лишь текстура, содержащая случайные значения (шум) для первого метода и еще один rendertarget для второго.
Итак, начнем!
1. Несколько выборок, вместо одной
Стандартная техника shadow maps во фрагментном шейдере обычно реализуется всего лишь одной выборкой из текстуры:
shadow2DProj(shadow_texture, shadow_proj_tc),
где shadow_proj_tc – это координаты в пространстве источника света. Давайте для удобства завернем это дело в функцию:
float SampleShadow(vec3 shadow_tc) { return shadow2D( shadow_texture, shadow_tc).x; }
Вместо использования shadow2DProj будет использовать shadow2D, а передавать в функцию значение shadow_proj_tc.xyz / shadow_proj_tc.w. Данная функция и будет нам реализовывать стандартную технику. Таким образом мы получим приведенную выше картинку. Теперь давайте подумаем: а что, если выборок будет не одна, а несколько? Улучшит ли это положение вещей?
Конечно да! Ведь если сделать несколько выборок вокруг центральной точки и потом усреднить результат – получим некое подобие размытия. Но так как для каждой точки мы будем сдвигать на постоянный шаг – то получим довольно однообразную тень, которая также будет иметь зазубрины на краях, хоть они будут и немного смазаны. Для того чтобы устранить этот эффект будем делать шаг в каждой точке в произвольном направлении. Для этого нам понадобится текстура, содержащая шум – случайные значения. Такую текстуру можно загружать из файла или генерировать в реальном времени (допустим, при инициализации). Генерирование конечно предпочтительнее, так как каждый раз будет новый шум, и освободиться немного места на диске.
Генерировать текстуру можно примерно так:
procedure BuildNoiseTexture(name:string; var texID:cardinal; dimension:integer; use_normal:boolean); var D : PRGBData; x : integer; v : Vector3; begin D:=AllocMem( dimension * dimension * 3 ); for x:=0 to pred( dimension*dimension) do begin v:=v.random_vec; if use_normal then v:=v.normalize; D^[x].r:=trunc( ( v.x * 0.5 + 0.5) * 255 ); D^[x].g:=trunc( ( v.y * 0.5 + 0.5) * 255 ); D^[x].b:=trunc( ( v.z * 0.5 + 0.5) * 255 ); end; BuildTexture2D( name, texID, dimension, dimension, GL_RGB, GL_RGB, GL_UNSIGNED_BYTE, D); ReallocMem( D, 0); end;
Эту текстуру и будем передавать в шейдер.
Теперь напишем функцию, которая будет реализовывать нам тень с несколькими выборками:
float SampleShadow(vec3 shadow_tc, vec2 noise, vec2 shadowmap_texel) { noise = vec2( 1.0); vec3 dx = vec3( noise.x * shadowmap_texel.x, 0.0, 0.0); vec3 dy = vec3( 0.0, noise.y * shadowmap_texel.y, 0.0); vec3 dxdy_p = ( dx + dy); vec3 dxdy_n = ( dx - dy); float result = shadow2D( shadow_texture, shadow_tc + dx).x + shadow2D( shadow_texture, shadow_tc - dx).x + shadow2D( shadow_texture, shadow_tc + dy).x + shadow2D( shadow_texture, shadow_tc - dy).x + shadow2D( shadow_texture, shadow_tc + dxdy_p).x + shadow2D( shadow_texture, shadow_tc - dxdy_p).x + shadow2D( shadow_texture, shadow_tc + dxdy_n).x + shadow2D( shadow_texture, shadow_tc - dxdy_n).x ; return 0.125 * result; }
Я предлагаю вообще не учитывать центральную точку – это даст нам еще более гладкие тени. Теперь необходимо пояснить – что мы будем передавать в эту функцию. Первый параметр нам уже знаком. Второй параметр – это прочитанный из текстуры шум. Третий параметр – это размер текселя карты теней – vec2(1.0 / sm_size_X, 1.0 / sm_size_Y). Из основного шейдера будем вызывать нашу функцию примерно таким образом:
vec2 noise = 2.0 * texture2D(noise_texture, gl_TexCoord[0].xy * vNoiseScale).xy – vec2( 1.0); float fShadow = SampleShadow( shadow_proj_tc.xyz / shadow_proj_tc.w, noise, shadow_texel.xy);
Где vNoiseScale – это количество повторений текстуры шума на экране. Можно вычислить как vec2(screen_size_X / noise_tex_size_X, screen_size_Y / noise_tex_size_Y).
Результатом работы такой техники будет тень, с претензией на мягкость:
2. Forward-render хорошо, а post-process-то лучше :)
Описанные выше методы работали в так называемой прямой отрисовке – то есть непосредственно при рисовании объектов. А что, если тени рассчитывать заранее, а при отрисовке объектов использовать уже рассчитанную текстуру? Какая от этого выгода? Выгода есть. Во первых – это возможность сгладить тени (проговорился, о чем пойдет речь дальше :)), во вторых – если шейдер объектов и так достаточно тяжелый, зачем его еще усложнять 8-ю выборками из текстуры? И потом не нужно будет для каждой вершины объекта вычислять матрицу перевода в пространство источника света. Плюс ко всему можно вычислить тени от нескольких источников света, и записать их в одну текстуру. Выгода очевидна! Теперь рассмотрим реализацию.
Все, что нам дополнительно понадобиться для такого расчета теней – это один полноэкранный прямоугольник и текстура, содержащая глубину сцены. Для того, чтобы нарисовать полноэкранный прямоугольник нет необходимости менять матрицы. Можно воспользоваться вот таким вершинным шейдером:
varying vec4 vertex; void main() { gl_TexCoord[0] = gl_MultiTexCoord0; gl_Position = vec4( gl_Vertex.x, gl_Vertex.y, gl_Vertex.z, 1.0); vertex = gl_Position; }
А рисовать его так:
glBegin(GL_QUADS); glTexCoord2f( 0.0, 0.0); glVertex3f( -1.0, -1.0, z); glTexCoord2f( 1.0, 0.0); glVertex3f( 1.0, -1.0, z); glTexCoord2f( 1.0, 1.0); glVertex3f( 1.0, 1.0, z); glTexCoord2f( 0.0, 1.0); glVertex3f( -1.0, 1.0, z); glEnd; // сейчас меня будут ругать за использование deprecated-функционала, ну да ладно :)
А вычислять тени мы будем непосредственно во фрагментном шейдере. Вот таком:
// текстура с глубиной сцены uniform sampler2D depth_texture; // shadow maps uniform sampler2DShadow shadow_texture; // текстура шума uniform sampler2D noise_texture; uniform vec4 distort_shadow_texel; // первые две компоненты – кол-во повторений текстуры шума на экране // вторые две – размер текселя карты теней uniform mat4 shadow_matrix; varying vec4 vertex; float SampleShadow(vec3 shadow_tc, vec2 noise, vec2 shadowmap_texel) { vec3 dx = vec3( noise.x * shadowmap_texel.x, 0.0, 0.0); vec3 dy = vec3( 0.0, noise.y * shadowmap_texel.y, 0.0); vec3 dxdy_p = ( dx + dy); vec3 dxdy_n = ( dx - dy); float result = shadow2D( shadow_texture, shadow_tc + dx).x + shadow2D( shadow_texture, shadow_tc - dx).x + shadow2D( shadow_texture, shadow_tc + dy).x + shadow2D( shadow_texture, shadow_tc - dy).x + shadow2D( shadow_texture, shadow_tc + dxdy_p).x + shadow2D( shadow_texture, shadow_tc - dxdy_p).x + shadow2D( shadow_texture, shadow_tc + dxdy_n).x + shadow2D( shadow_texture, shadow_tc - dxdy_n).x ; return 0.125 * result; } void main( ) { // получаем глубину сцены в точке float fDepth = texture2D( depth_texture, gl_TexCoord[0].st).x; // получаем вектор с шумом vec2 noise = 2.0*texture2D( noise_texture, gl_TexCoord[0].xy*distort_shadow_texel.xy).xy - vec2( 1.0); // восстанавливаем мировые координаты точки // нам очень повезло, что мы не меняли матрицы при отрисовке прямоугольника :) vec4 inv_proj = gl_ModelViewProjectionMatrixInverse * vec4( vertex.xy, 2.0 * fDepth - 1.0, 1.0); // находим координаты точки в пространстве источника света vec4 shadow_proj = shadow_matrix * ( inv_proj / inv_proj.w); // вычисляем тень float fShadow = SampleShadow( shadow_proj.xyz / shadow_proj.w, noise, distort_shadow_texel.zw); gl_FragColor = vec4( fShadow, 1.0, 1.0, 1.0); }
В полученной текстуре у нас будет содержаться что-то вроде:
Теперь можно отказаться от вычисления теней при отрисовке объектов, а использовать полученную текстуру по аналогии с технологией “Light maps”. Таким образом, шейдер отрисовки объектов превращается в нечто подобное:
uniform sampler2D lightmap_texture; … vec2 proj_tc = 0.5 * proj_coords.xy / proj_coords.w + 0.5; float fShadow = texture2D(lightmap_texture, proj_tc).x;
где proj_coords – проективные координаты.
Таким образом мы избавились от вычисления теней непосредственно при отрисовке объектов и плюс к этому тени у нас храняться в отдельной текстуре, которую нам никто не мешает правильно размыть. Используя размытие с учетом глубины, мы получим самые настоящие мягкие тени. Вот такие:
Конечно, такие тени потребуют дополнительных вычислений, но, я думаю, оно того стоит. Причем самым сложным в вычислительном плане здесь будет размытие текстуры с тенями. Таким образом, мы добились мягких теней, с относительно небольшими затратами. Надеюсь, статья была полезной. Удачи!
#GLSL, #графика, #OpenGL, #тени, #мягкие тени
19 июня 2009