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

Реализация карт теней с использованием GLSL шейдеров (3 стр)

Автор:

Матрица источника света

Перед тем как рендерить сцену с тенями, мы вычисляем матрицу источника света и передаём в шейдер. Матрица получается, как вы уже видели, следующим образом:

  glLoadIdentity();
  glTranslatef(0.5, 0.5, 0.5); // + 0.5
  glScalef(0.5, 0.5, 0.5); // * 0.5
  glMultMatrixf(lightProjectionMatrix);
  glMultMatrixf(lightModelViewMatrix);
  glMultMatrixf(cameraInverseModelViewMatrix);
  glGetFloatv(GL_MODELVIEW_MATRIX, lightMatrix);

glTranslatef(0.5, 0.5, 0.5) и glScalef(0.5, 0.5, 0.5) нам нужны для того, чтобы преобразовать вершину, которая получится в результате умножения на матрицу источника света, из диапазона [-1..1] в диапазон [0..1]. Если этого не сделать через матрицу, нам придётся это сделать в фрагментном шейдере так:

  vec3 smcoord = lpos.xyz / lpos.w * 0.5 + 0.5;

Что как видно добавляет в шейдер лишние операции на каждый фрагмент в виде * 0.5 + 0.5. А это нам не к чему.

Умножение на инвертированную видовую матрицу камеры нужно для того, чтобы отменить видовые преобразования, которые передаются в шейдер, вместе с модельной матрицей, через gl_ModelViewMatrix. Дело в том, что модельная матрица и видовая объединены в одну матрицу, но когда мы хотим перевести нашу вершину в пространство света, то видовые преобразования камеры нам не нужны. Можно поступить иначе, передавать для каждого объекта в шейдер модельную матрицу, в этом случае не будет нужды в инвертированной матрице камеры.

Код инвертирования достаточно прост:

void InverseMatrix(float dst[16], float src[16])
{
  dst[0] = src[0];
  dst[1] = src[4];
  dst[2] = src[8];
  dst[3] = 0.0;
  dst[4] = src[1];
  dst[5] = src[5];
  dst[6]  = src[9];
  dst[7] = 0.0;
  dst[8] = src[2];
  dst[9] = src[6];
  dst[10] = src[10];
  dst[11] = 0.0;
  dst[12] = -(src[12] * src[0]) - (src[13] * src[1]) - (src[14] * src[2]);
  dst[13] = -(src[12] * src[4]) - (src[13] * src[5]) - (src[14] * src[6]);
  dst[14] = -(src[12] * src[8]) - (src[13] * src[9]) - (src[14] * src[10]);
  dst[15] = 1.0;
}

Данная функция не является универсальной и подойдёт не для любой матрицы, но в нашем примере этого достаточно.

Хочу обратить внимание, что в примере используется перспективный источник света. При ортогональном источнике света практически всё аналогично, единственное отличие заключается в том, что не обязательно делить на w.

Шейдер сцены с тенью

Давайте взглянем на вершинный шейдер, в котором мы умножаем полученную ранее матрицу источника света:

#version 120

uniform mat4 lightMatrix;
uniform vec3 lightPos;
uniform vec3 lightDir;

varying vec4 lpos;
varying vec3 normal;
varying vec3 light_vec;
varying vec3 light_dir;

void main(void)
{
  vec4 vpos = gl_ModelViewMatrix * gl_Vertex;
  lpos = lightMatrix * vpos;
  gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;

  light_vec = vpos.xyz - lightPos;
  light_dir = gl_NormalMatrix * lightDir;
  normal = gl_NormalMatrix * gl_Normal;
  gl_FrontColor = gl_Color;
}

lpos - вершина в пространстве света.
light_vec, light_dir, normal, gl_FrontColor нужны для расчёта освещения и светового пятна. Так как освещение выходит за рамки статьи, то описывать расчёт света я не буду. Вы можете самостоятельно с этим разобраться.

Теперь рассмотрим фрагментный шейдер для варианта #1:

#version 120

uniform sampler2D shadowMap;

varying vec4 lpos;
varying vec3 normal;
varying vec3 light_vec;
varying vec3 light_dir;

const float inner_angle = 0.809017;
const float outer_angle = 0.707107;

void main (void)
{
  vec3 smcoord = lpos.xyz / lpos.w;
  float shadow = float(smcoord.z <= texture2D(shadowMap, smcoord.xy).x);

  vec3 lvec = normalize(light_vec);
  float diffuse = max(dot(-lvec, normalize(normal)), 0.0);
  float angle = dot(lvec, normalize(light_dir));
  float spot = clamp((angle - outer_angle) / (inner_angle - outer_angle), 0.0, 1.0);
  gl_FragColor = vec4(gl_Color.xyz * diffuse * shadow * spot, 1.0);
}

Делением на w мы переводим lpos из однородных координат в декартовы, в результате x и y послужат нам текстурными координатами для выборки с текстуры глубины, а z — глубина фрагмента сцены. Далее мы эту z сравниваем со значением глубины в текстуре, тем самым мы определим какие фрагменты затенены, а какие нет.
shadow в данном случае будет либо 0.0 (тень), либо 1.0 (свет).

Затем идёт вычисление света и светового пятна. inner_angle и outer_angle - это углы в радианах для светового пятна.

Взглянем на шейдер варианта #2:

#version 120

uniform sampler2DShadow shadowMap;

varying vec4 lpos;
varying vec3 normal;
varying vec3 light_vec;
varying vec3 light_dir;

const float inner_angle = 0.809017;
const float outer_angle = 0.707107;

void main (void)
{
  vec3 smcoord = lpos.xyz / lpos.w;
  float shadow = shadow2D(shadowMap, smcoord).x;

  vec3 lvec = normalize(light_vec);
  float diffuse = max(dot(-lvec, normalize(normal)), 0.0);
  float angle = dot(lvec, normalize(light_dir));
  float spot = clamp((angle - outer_angle) / (inner_angle - outer_angle), 0.0, 1.0);
  gl_FragColor = vec4(gl_Color.xyz * diffuse * shadow * spot, 1.0);
}

Как видно разница небольшая. Нет сравнения в шейдере и вместо texture2D() используется shadow2D(). Функция shadow2D() вместо нас осуществляет сравнение, для этого мы передаём ей, кроме x и y, ещё и z. Функция выборки из текстуры вернёт нам не значение текстуры, а результат сравнения. Обратите внимание, что самплер у нас вместо sampler2D стал теперь sampler2DShadow.

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

Полезными могут оказаться проекционные аналоги функций texture2D() и shadow2D(), соответственно называются они texture2DProj() и shadow2DProj(). Отличие заключается в том, что texture2DProj() и shadow2DProj() принимают декартовы координаты, вместо однородных. Это означает, что нет нужды координаты делить на w, за нас это сделает функция.

Взгляните, как бы выглядел вариант #1, если бы мы использовали texture2DProj():

  ...
  float shadow = float((lpos.z / lpos.w) <= texture2DProj(shadowMap, lpos).x);
  ...

Как видите, мы избавились от деления на w координат x и y, но z всё таки нам пришлось делить.

Если мы передадим функции texture2DProj() трёх-компонентный вектор, то деление координат x и y будет производиться на z, в случае с четырёх-компонентным (как у нас lpos), третий компонент (то есть z) функция проигнорирует, и деление будет производиться на w.

Ситуация выглядит лучше с shadow2DProj():

  ...
  float shadow = shadow2DProj(shadowMap, lpos).x;
  ...

В этом случае на w у нас поделятся все три компоненты: x, y и z.

В примере используется только texture2D() и shadow2D() функции. Шейдеры называются shadow.vert, shadow_tex2D.frag и shadow_shad2D.frag.

Страницы: 1 2 3 4 Следующая »

#GLSL, #OpenGL, #shader, #тени

1 февраля 2009 (Обновление: 14 сен. 2009)

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