Войти
ПрограммированиеСтатьиГрафика

Основы создания мягких теней (GLSL)

Автор:

Данная статья является естественным продолжением и дополнением статьи «Реализация карт теней с использованием GLSL шейдеров» Дмитрия Беспалова (aka Executor).

Часто разработчикам хочется использовать в сцене так называемые «мягкие» тени, то есть такие, которые бы не имели артефактов техники shadow maps — зазубрин на краях тени.

Введение
1. Несколько выборок, вместо одной
2. Forward-render хорошо, а post-process-то лучше :)

Введение

Чтобы лучше понять, о чем идет речь, и с чем мы будем бороться, посмотрим на картинку:

Ступенчатая тень | Основы создания мягких теней (GLSL)

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

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

Что нам понадобиться кроме текстуры с глубиной сцены: всего лишь текстура, содержащая случайные значения (шум) для первого метода и еще один 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).

Результатом работы такой техники будет тень, с претензией на мягкость:

Тень, с претензией на мягкость | Основы создания мягких теней (GLSL)

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);
}

В полученной текстуре у нас будет содержаться что-то вроде:

Текстура с тенью | Основы создания мягких теней (GLSL)

Теперь можно отказаться от вычисления теней при отрисовке объектов, а использовать полученную текстуру по аналогии с технологией “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)

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

#GLSL, #графика, #OpenGL, #тени, #мягкие тени

19 июня 2009

Комментарии [31]