OpenGL community
GameDev.ru / Сообщества / OpenGL community / Журнал / Реализация ESM (exponential shadow maps) ver.2

Реализация ESM (exponential shadow maps) ver.2

Автор:

Небольшая заметка про реализацию ESM (exponential shadow maps) на OpenGL с использованием GLSL.

Введение

В первую очередь - зачем? Зачем делать эту заметку - ESM техника капризная, есть и другие интересные техники и т.д. и т.п. Ответ простой - интерес. Просто мне захотелось реализовать эту технику, посмотреть на результаты, покрутить настройки в конце-концов. Т.к. толковых примеров по реализации ESM с использованием GLSL я в интернете не нашел (плохо искал?), то решил идти частично от теория, частично от здравого смысла, частично от советов коллег по цеху. Результат в каком-то виде я получил, дальше пока не разбирался, просто хочу описать то что уже готово, дальше уже каждый сам решит, интересует его эта тема или нет.

кликабельно

Изображение

Примечания ко второй редакции

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

Что такое ESM

ESM (exponential shadow maps) техника предложенная Марко Сальви (Marco Salvi), основывается на сглаживании пороговой функции для теста затенения с помощью экспоненциальной функции. Использование сглаженного порога позволяет получить сглаживание тени на ее границе без использования дополнительных выборок, в отличии от PCF. Также, в отличии от простой SM (shadow map), где мы используем карту глубины (depth map), для реализации этой техники нам понадобится любая float-текстура для хранения одного канала (например GL_R32F). В этой текстуре мы будем хранить значения той самой экспоненциальной функции, которая вычисляется на основе линейной глубины фрагмента. Позже, на шаге основного рендера, мы используем данные этой текстуры для определения находится ли конкретный фрагмент в тени или нет. Как дополнение к этой технике мы можем сгладить float-текстуру перед использованием каким-нибудь фильтром, например размытием по гауссу, чтобы получить еще более мягкие тени и убрать артефакты.

Реализация

У нас есть направленный источник света light, нам известная его позиция lightPosition, его направление lightDirection. Мы уже составили для нашего источника света матрицу вида lightViewMat и матрицу проекции lightProjMat, дополнительно вычислим для этого источника света матрицу перевода в текстурные координаты:

const mat4 bias(
  0.5f, 0.0f, 0.0f, 0.5f,
  0.0f, 0.5f, 0.0f, 0.5f,
  0.0f, 0.0f, 0.5f, 0.5f,
  0.0f, 0.0f, 0.0f, 1.0f
);

const mat4 lightTexMat = bias * lightProjMat * lightViewMat;

Хорошо, все необходимые данные для источника света у нас имеются, теперь нам надо произвести рендер с позиции источника света и заполнить float-текстуру значениями экспоненциальной функции.

Первый шаг - создание FBO и текстур к нему:

GLuint exp_fbo, exp_texture, depth_texture;
GLenum status;

glActiveTexture(GL_TEXTURE0);

glGenTextures(1, &exp_texture);
glBindTexture(GL_TEXTURE_2D, exp_texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, windowWidth2, windowHeight2, 0, GL_RED, GL_FLOAT, NULL);

glGenTextures(1, &depth_texture);
glBindTexture(GL_TEXTURE_2D, depth_texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, windowWidth2, windowHeight2, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);

glGenFramebuffers(1, &exp_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, exp_fbo);

glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, exp_texture,   0);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,  depth_texture, 0);

status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE)
{
  LOG_ERROR("Framebuffer error 0x%X\n", status);
  return false;
}

Второй шаг - шейдеры для рендера экспоненциальной функции. Вершинный шейдер:

#version 330 core

layout(location = 0) in vec3 position;

// на этом шаге используется матрица источника света
uniform struct Transform
{
  mat4 modelViewProjMat;
} transform;

out Vertex
{
  vec4 position;
} vertex;

void main(void)
{
  vertex.position = transform.modelViewProjMat * vec4(position, 1.0);
  gl_Position     = vertex.position;
}

Фрагментный шейдер:

#version 330 core

in Vertex
{
  vec4 position;
} vertex;

layout(location = 0) out float color;

void main(void)
{
  const float esm_factor = 0.5;
  color = exp(esm_factor * vertex.position.z);
}

Отдельно стоит сказать про esm_factor, смысл в том, что этот параметр подбирается в зависимости от сцены, главное чтобы при вычислении функции exp() не произошло переполнение float-значений. С другой стороны, если вы используете слишком маленькое значение - в результате вы получите слишком мягкие тени с артефактами. Так что тут придется экспериментировать :)

Уже можно производить рендер сцены с позиции источника света. Настраиваем камеру наблюдателя в соответствии с матрицами источника света и используем вышеприведенные шейдеры. На выходе в текстуре exp_texture мы получим интересующие нас значения. Впринципе именно сейчас нам нужно применить какой-нибудь фильтр к текстуре exp_texture, но на этом я останавливаться сейчас не буду, но пример есть в исходных кодах к этой заметке. Это кстати одна из особенностей ESM - можно пользоваться фильтрами для сглаживания тени.

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

#version 330 core

layout(location = 0) in vec3 position;
layout(location = 1) in vec3 normal;
layout(location = 2) in vec2 texcoord;

uniform struct Transform
{
  vec3 viewPos;
  mat4 viewProjMat;
  mat3 normalMat;
  mat4 modelMat;
  mat4 lightTexMat;
  mat4 lightViewProjMat;
} transform;

uniform struct Light
{
  vec4 ambient;
  vec4 diffuse;
  vec4 specular;
  vec4 position;
} light;

out Vertex
{
  vec2 texcoord;
  vec4 smcoord;
  vec3 normal;
  vec3 lightDir;
  vec4 lightPos;
  vec3 viewDir;
  vec4 distance;
} vertex;

void main(void)
{
  vec4 vertPosition = transform.modelMat * vec4(position, 1.0);
  vertex.texcoord   = texcoord;
  vertex.distance   = transform.lightViewProjMat * vertPosition;
  vertex.smcoord    = transform.lightTexMat * vertPosition;
  vertex.normal     = transform.normalMat * normal;
  vertex.lightPos   = light.position;
  vertex.lightDir   = normalize(light.position.xyz);
  vertex.viewDir    = normalize(transform.viewPos - vertPosition.xyz);

  gl_Position = transform.viewProjMat * vertPosition;
}

Фрагментный шейдер:

#version 330 core

uniform sampler2D expTexture;

uniform struct Light
{
  vec4 ambient;
  vec4 diffuse;
  vec4 specular;
  vec4 position;
} light;

uniform struct Material
{
  vec4  ambient;
  vec4  diffuse;
  vec4  specular;
  vec4  emission;
  float shininess;

  sampler2D diffuseTexture;
} material;

in Vertex
{
  vec2 texcoord;
  vec4 smcoord;
  vec3 normal;
  vec3 lightDir;
  vec4 lightPos;
  vec3 viewDir;
  vec4 distance;
} vertex;

layout(location = 0) out vec4 color;

void main(void)
{
  const float esm_bias   = 0.01;
  const float esm_factor = 0.5;

  color = texture(material.diffuseTexture, vertex.texcoord);

  vec3  normal   = normalize(vertex.normal);
  vec3  lightDir = normalize(vertex.lightDir);
  vec3  viewDir  = normalize(vertex.viewDir);

  float NdotL    = max(dot(normal, lightDir), 0.0);
  float RdotVpow = max(pow(dot(reflect(-lightDir, normal), viewDir), min(10.0, material.shininess)), 0.0);

  float occluder = texture(expTexture, vertex.smcoord.xy / vertex.smcoord.w).r;
  float receiver = exp(esm_bias - esm_factor * vertex.distance.z);
  float shadow   = clamp(occluder * receiver, 0.3, 1.0);

  color *= (material.emission + material.ambient * light.ambient
    + material.diffuse * light.diffuse * NdotL
    + material.specular * light.specular * RdotVpow) * shadow;
}

Обратите особое внимание на две переменные: esm_bias по факту служит для предотвращения артефактов с самозатенением, с этим параметром тоже надо играться, второй параметр esm_factor должен быть строго таким же как и его напарник из шейдера построения текстур exp_texture, иначе будет очень страшно.

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

Плюсы и минусы

Много говорить не буду, плюсы понятны - сглаженные мягкие тени, минусы - слишком мягкие тени, чем ближе кастер к приемнику тем мягче тень, бывает ее вообще не видно. Еще одним недостатком является то, что ESM плохо дружит с незамкнутыми сценами, поэтому используйте какой-нибудь sky-sphere, чтобы exp_texture не была "пустой" в отдельных местах. Пожалуй самый главный минус ESM - light leaking, тут надо играться с параметрами и глубже погружаться в тему, думаю это уже на самостоятельное изучение.

Полезные ссылки

Исходники и демо


OpenGL ESM directional light demo

OpenGL ESM omni light demo

ESM omni light demo w/blur

22 мая 2011

#GLSL, #OpenGL


Обновление: 10 января 2015

2001—2018 © GameDev.ru — Разработка игр