Хабрахабр

[Перевод] Learn OpenGL. Урок 5.3 — Карты теней

Если лучи от источника света не попадают на объект, так как поглощаются другим объектом, то первый объект находится в тени. Тень — это отсутствие света. Благодаря ним сцена приобретает "глубину". Тени добавляют реализма к изображению и дают увидеть взаимное расположение объектов. Сравните следующие изображения сцены с тенями и без:

with_shadows_and_without

Благодаря теням видно, что один из кубов висит в воздухе. Как можно заметить, тени делают намного более очевидным то, как объекты расположены друг относительно друга.

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

Карты теней не так уж и трудно понять, они довольно дёшевы с точки зрения производительности и их легко улучшить до более продвинутых алгоритмов (типа теней от точечного источника света или каскадных карт теней) Один из методов — карты теней (shadow maps) — относительно простой в реализации, используется в большинстве видеоигр и даёт достойные результаты.

Содержание

Если вы их знаете — не будет заблуждений, не знаете — запоминайте вместе с оригинальным термином, в интернете куча хороших статей на английском и довольно мало на русском) (Примечение переводчика — в дальнейшем некоторые русские версии терминов будут дублироваться устоявшимся английским вариантом.

Карты теней

Всё, что мы видим, освещено, остальное — в тени. Идея, лежащая в основе карт теней, достаточно проста: мы рисуем сцену с точки зрения источника света. Так как источник света "видит" куб, а не кусочек пола, эта часть пола будет затенена. Представьте кусочек пола с большим кубом между ней и источником света.

shadow_mapping_theory

Закрытые поверхности нарисованы чёрным — они будут нарисованы затенёнными. На картинке выше синими линиями нарисованы поверхности, которые источник света может увидеть. Из-за этого левая поверхность висящего кубика освещена, в отличие от куба справа. Если нарисовать линию (луч) от источника света вершине самого правого куба, то она сначала пересечёт висящий в воздухе кубик.

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

Значение в буфере глубины- это глубина фрагмента из точки зрения камеры, ограниченная значениями от 0 до 1. Возможно Вы уже читали про тест глубины: перевод на хабре, оригинал. Таким образом мы получим наименьшие значения глубины, которые видно с точки зрения источника света. Что если мы отрендерим сцену с точки зрения источника света и сохраним значения глубины в текстуру? Такую текстуру называют картой глубины (depth map) или картой теней (shadow map). Кроме того, значения глубины показывают поверхности, ближайщие для источника света.

shadow_mapping_theory_spaces

С помощью значений глубины, сохранённых в текстуру, мы находим ближайшую к источнику поверхность и с её помощью определяем, что находится в тени. На левой картинке показан направленый источник света (все лучи параллельны), отбрасывающий тень на поверхность ниже куба. Мы создаём карту глубины с помощью рендеринга сцены, в качестве матриц вида и проекции используя матрицы, соответствующие нашему источнику света.

Тем не менее, ради создания карты теней нам придётся рисовать сцену из позиции на линии направления света. Направленный свет с параллельными лучами не имеет позиции и находится как бы "бесконечно далеко".

переводчика — openGL отсекает поверхности, которые слишком далеко (z > 1) или слишком близко (z < 0 или z < -1 в зависимости от настроек)). Прим. С математической точки зрения позиции нет, но реально позицией камеры можно считать точку, которая при рисовании отображаеся в маскимально близкую точку по центру экрана Матрица камеры выбирается так, чтобы z координата для объектов на сцене была в этом интервале, иначе мы их не увидим.

Мы рисуем фрагмент поверхности в точке P, и нам надо определить, находится ли он в тени. На картинке справа мы видим тот же самый свет, куб и наблюдателя. Так как точка P не видна из точки зрения света, её координата z в нашем примере будет 0. Для этого мы переводим P в координатное пространство источника света T(P). По координатам точки x,у мы можем заглянуть в карту глубины и узнать, что ближайшая к источнику света точка — С с глубиной 0. 9. 4 Это значение меньше, чем для точки P, поэтому точка P находится в тени.

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

Карта глубины

Карта глубины — это текстура со значениями глубины, отрендеренная с точки зрения источника света. В первом проходе мы сгенерируем карту глубины. Чтобы сохранить отрендеренный результат в текстуру, нам понадобится кадровый буфер (framebuffer): перевод на хабре, оригинал. Мы потом будем использовать её для вычисления теней.

Сначала создадим кадровый буфер для рисования карты глубины:

unsigned int depthMapFBO;
glGenFramebuffers(1, &depthMapFBO);

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

const unsigned int SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024; unsigned int depthMap;
glGenTextures(1, &depthMap);
glBindTexture(GL_TEXTURE_2D, depthMap);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

Так как нас интереуют только значения глубины (а не цвета r,g,b,a), мы указываем формат текстуры GL_DEPTH_COMPONENT. Создание карты глубины не выглядит сложным. Устанавливаем высоту и ширну текстуры 1024*1024 — это будет размер карты глубины.

Теперь присоединим текстуру глубины к кадровому буферу в качестве буфера глубины

glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthMap, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);

Кадровый буфер будет неполным без буфера цвета, поэтому мы должны явно указать, что не собираемся рендерить цвет. Когда мы рисуем сцену с точки зрения источника света, нас интересует только глубина, буфер цвета не нужен. Для этого мы устанавливаем GL_NONE для glDrawBuffer и glReadBuffer.

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

// 1. сначала рисуем карту глубины
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO); glClear(GL_DEPTH_BUFFER_BIT); ConfigureShaderAndMatrices(); RenderScene(); // 2. рисуем сцену как обычно с тенями (используя карту глубины)
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); ConfigureShaderAndMatrices(); glBindTexture(GL_TEXTURE_2D, depthMap); RenderScene();

Обратитке внимание на вызовы glViewport: обычно размер карты глубины отличается от размера экрана (или текстуры, в которую рендерится итоговое изображение). Этот код не содержит некоторых подробностей, но он даёт общую идею карт теней. Если вы забудете его поменять, то текстуре глубины будет обновлён только квадратный кусочек с размером экрана или (если текстура меньше) часть информации на ней не будет отображена (останется за краями).

Пространство источника света

Однако, в первом проходе мы используем иные матрицы для проекции и вида: для рисования сцены с точки зрения источника света. Единственное, что неизвестно в коде выше — что делает функция ConfigureShaderAndMatrices()?
Во втором проходе она работает как обычно — устанавливает соответствующие матрицы вида и проекции для камеры и матрицы модели для объектов.

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

float near_plane = 1.0f, far_plane = 7.5f;
glm::mat4 lightProjection = glm::ortho(-10.0f, 10.0f, -10.0f, 10.0f, near_plane, far_plane);

Так как матрица проекции определяет расстояние, на котором вы будете видеть объекты (т.е., их не будет отсекать видеокарта как слишком близкие или далёкие), стоит убедиться, что размер области отсечения содержит все объекты, которые Вы хотите отобразить в карте глубины. Это пример матрицы ортографической проекции, используемый в демо к этой статье.

Чтобы создать матрицу вида, в котором будут видны все объекты с точки зрения источника света, мы будет использовать непопулярную функцию glm::lookAt, сейчас источник света "смотрит" в центр сцены.

glm::mat4 lightView = glm::lookAt(glm::vec3(-2.0f, 4.0f, -1.0f), glm::vec3( 0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 1.0f, 0.0f));

пер. (Прим. — первый вектор — расположение камеры, второй — куда она смотрит, третий — направление взгляда вверх)

Это именно то, что нужно нам для рендеринга карты глубины. Комбинация этих двух матриц даёт нам матрицу преобразования из координат мира в координаты, в которых источник света "видит" мир.

glm::mat4 lightSpaceMatrix = lightProjection * lightView;

С этой матрицей мы может отреднерить сцену как обычно, используя её вместо матриц вида и проекции обычной камеры. Матрица lightSpaceMatrix — это как раз то, что выше мы обозначали как T. Поэтому мы напишем максимально простой шейдер для рисования только карты глубины. Однако, нас интересуют только значения глубины, и хочется сохранить производительность, не производя лишних вычислений для неиспользуемого цвета.

Рендеринг в карту глубины

Для такого простого шейдера (назовём его simpleDepthShader) напишем вершинный шейдер: Когда мы рендерим сцену для источника света, нам нужны только координаты вершин и ничего больше.

#version 330 core
layout (location = 0) in vec3 aPos; uniform mat4 lightSpaceMatrix;
uniform mat4 model; void main()
{ gl_Position = lightSpaceMatrix * model * vec4(aPos, 1.0);
}

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

#version 330 core void main()
{ // gl_FragDepth = gl_FragCoord.z;
}

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

Рисование в буфер глубины превращается в следующий код:

simpleDepthShader.use();
glUniformMatrix4fv(lightSpaceMatrixLocation, 1, GL_FALSE, glm::value_ptr(lightSpaceMatrix)); glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO); glClear(GL_DEPTH_BUFFER_BIT); RenderScene(simpleDepthShader);
glBindFramebuffer(GL_FRAMEBUFFER, 0);

Функция RenderScene принимает шейдер, вызывает необходимые для рисования функции и утснавливает матрицы модели при необходимости.

Можно спроецировать эту текстуру на прямоугольник размером с экран и показать её. В результате у нас заполненный буфер глубины, для каждого пикселя содержащий глубину ближайщего фрагмента с точки зрения света. перевод на хабре, оригинал.) (схоже с тем, что было в постобработке в примере c кадровым буфером.

shadow_mapping_depth_map

Для рисования карты глубины на прямоугольнике используется такой шейдер:

#version 330 core
out vec4 FragColor; in vec2 TexCoords; uniform sampler2D depthMap; void main()
{ float depthValue = texture(depthMap, TexCoords).r; FragColor = vec4(vec3(depthValue), 1.0);
}

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

Исходный код рендеринга сцены в карту глубины можно увидеть здесь.

Рисование теней

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

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords; out VS_OUT { vec3 FragPos; vec3 Normal; vec2 TexCoords; vec4 FragPosLightSpace;
} vs_out; uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
uniform mat4 lightSpaceMatrix; void main()
{ vs_out.FragPos = vec3(model * vec4(aPos, 1.0)); vs_out.Normal = transpose(inverse(mat3(model))) * aNormal; vs_out.TexCoords = aTexCoords; vs_out.FragPosLightSpace = lightSpaceMatrix * vec4(vs_out.FragPos, 1.0); gl_Position = projection * view * model * vec4(aPos, 1.0);
}

Мы принимаем ту же самую lightSpaceMatrix, что использовалась в первом проходе для рисования глубины, и с её помощью переводим вектор в пространство источника света. Из нового здесь — дополнительный вектор FragPosLightSpace на выходе вершинного шейдера. FragPos) и в пространстве источника света (vs_out. Вершинный шейдер передаёт во фрагментный одновременно коодринаты вершины и в пространстве текущей камеры (vs_out. FragPosLightSpace).

Во фрагментном шейдере мы найдём значение shadow — оно будет равно 1. Мы будем использовать фрагментный шейдер, основанный на световой модели Блинна-Фонга. 0 для освещённого. 0, если фрагмент находится в тени и 0. 0 - shadow). Результирующие diffuse и specular цвета (диффузное и зеркальное освещение) будут домножаться на (1. Тени редко полностью чёрные по причине непрямого освещения, поэтому фоновое освещение будет присутствовать независимо от тени.

#version 330 core
out vec4 FragColor; in VS_OUT { vec3 FragPos; vec3 Normal; vec2 TexCoords; vec4 FragPosLightSpace;
} fs_in; uniform sampler2D diffuseTexture;
uniform sampler2D shadowMap; uniform vec3 lightPos;
uniform vec3 viewPos; float ShadowCalculation(vec4 fragPosLightSpace)
{ [...]
} void main()
{ vec3 color = texture(diffuseTexture, fs_in.TexCoords).rgb; vec3 normal = normalize(fs_in.Normal); vec3 lightColor = vec3(1.0); // ambient vec3 ambient = 0.15 * color; // diffuse vec3 lightDir = normalize(lightPos - fs_in.FragPos); float diff = max(dot(lightDir, normal), 0.0); vec3 diffuse = diff * lightColor; // specular vec3 viewDir = normalize(viewPos - fs_in.FragPos); float spec = 0.0; vec3 halfwayDir = normalize(lightDir + viewDir); spec = pow(max(dot(normal, halfwayDir), 0.0), 64.0); vec3 specular = spec * lightColor; // calculate shadow float shadow = ShadowCalculation(fs_in.FragPosLightSpace); vec3 lighting = (ambient + (1.0 - shadow) * (diffuse + specular)) * color; FragColor = vec4(lighting, 1.0);
}

Это по большей части копия шейдера, который мы использовали в примере с освещением: перевод на хабре, advanced lighting.

Основную часть работы делает функция ShadowCalculation. Здесь добавлено только вычисление тени. 0 — shadow) — т.е., в зависимости от того, насколько сильно фрагмент не затенён. В конце фрагментного шейдера мы домножаем вклад от диффузного и зеркального отражение света на (1. Кроме того, этот шейдер на вход дополнительно принимает позицию фрагмента в пространстве источника света и текстуру со значениями глубины (которая была отрендерена в первом проходе).

Когда мы возвращаем позицию вершины в gl_Position в вершинном шейдере, openGL автоматически делит x,y,z на w, чтобы корректно работала перспектива. Чтобы проверить, находится ли фрагмент в тени, приведём позицию к в пространстве источника света к нормализованным координатам. Так как FragPosLightSpace передаётся не в как gl_Position, нам придётся сделать это деление самим.

float ShadowCalculation(vec4 fragPosLightSpace)
{ // perform perspective divide vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w; [...]
}

Получаем позицию фрагмента в пространстве источника света.

0 не изменяется и деление на w становится не нужным. При использовании ортографической проекции координата w=1. Но деление необходимо при использовании перспективной проекции, и наш код будет корректно работать для обоих случаев.

пер. (Прим. Вот в этой статье на первой картинке показана разница между линейной и перспективной интерполяцией для текстурных координат.) — деление на w необходимо делать именно в фрагментном шейдере.

Приведём их к интервалу [0,1]: Текстурные координаты лежат в интервале [0,1], а координаты видимых фрагментов при рендеринге принимают значения в [-1,1].

projCoords = projCoords * 0.5 + 0.5;

По этим координатам мы можем посмотреть значение глубины в текстуре — это будет глубина ближайщего к источнику света объекта.

float closestDepth = texture(shadowMap, projCoords.xy).r;

Чтобы получить глубину текущего фрагмента, мы просто берём его координату z в пространстве источника света.

```с
float currentDepth = projCoords.z;

После этого простым сравнением ```currentDepth``` и ```closestDepth``` можно определить, является ли наш фрагмент ближайщим или лежит в тени. ```c
float shadow = currentDepth > closestDepth ? 1.0 : 0.0;

Весь код функции ShadowCalculation получается таким:

float ShadowCalculation(vec4 fragPosLightSpace)
{ // perform perspective divide vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w; // transform to [0,1] range projCoords = projCoords * 0.5 + 0.5; // get closest depth value from light's perspective (using [0,1] range fragPosLight as coords) float closestDepth = texture(shadowMap, projCoords.xy).r; // get depth of current fragment from light's perspective float currentDepth = projCoords.z; // check whether current frag pos is in shadow float shadow = currentDepth > closestDepth ? 1.0 : 0.0; return shadow;
}

Использование этого шейдера вместе с текстурами и обычными матрицами вида и проекции для второго прохода рендера даст результат примерно как на картинке:

shadow_mapping_shadows

Исходный код демо. Если Вы сделали всё правильно, вы увидите тени на полу и кубиках (впрочем, с некоторыми артефактами).

Улучшаем карты теней

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

Муаровый узор

Увеличенная картинка напоминает муаровый узор. Очевидно, в изображении ниже что-то неправильно.

shadow_mapping_acne

Этот эффект может быть объяснён с помощью одной каринки: Весь пол покрыт явно заметными чередующимися чёрными полосками.

shadow_mapping_acne_diagram

пер. (Прим. Я не нашёл устоявшегося перевода. — для описания эффекта автор использует термин "shadow acne". Я мог бы перевести это как "теневые точки", но в русскоязычном интернете по этому термину всё равно ничего не найти)

На картинке выше показан пол, на котором каждый наклонный кусочек (снизу слева вверх на право) представляет собой один тексель из карты глубины. Так как у карты теней ограниченное разрешение (в примере выше мы использовали текстуру 1024*1024), несколько пикслей на итоговом изоражении могут получить одно и то же значение из карты глубины. (тексель — текстурный пиксель)

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

Мы можем решить это проблему с помощью маленького хака — сдвиг значений глубины на маленькую величину (shadow bias), чтобы все фрагменты были над поверхностью.

shadow_mapping_acne_bias

пер. (Прим. Чтобы фрагмент не был затенён, зигзагообразные линии, описывающие значения из карты глубины, должны быть ниже поверхности) — у меня сильное чувство, что картинки нарисованы некорректно.

float bias = 0.005;
float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;

005 для нашей сцены в целом решает проблему, но некоторые поверхности, на которые свет падает под очень маленьким углом, всё ещё будут иметь полосочки тени. Сдвиг на 0. Воспользуемся скалярным произведением. Более серьёзным подходом будет изменение сдвига в зависимости от угла, под которым свет падает на поверхность.

float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);

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

shadow_mapping_with_bias

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

Эффект Питера Пэна

(Piter Panning)

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

shadow_mapping_peter_panning

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

Мы не заметим неправильных результатов, так как нам не важно, есть ли тени внутри объекта — их всё равно не видно. В первом проходе рендеринга нам нужны только значения глубины, и нам не важно, какую глубину взять — от фронтальной поверхности или от обратной.

shadow_mapping_culling

Обратите внимание, что необходимо включить GL_CULL_FACE. Чтобы убрать эффект Питера Пэна, мы отсекаем фронтальные грани на первом проходе.

glCullFace(GL_FRONT);
RenderSceneToDepthMap();
glCullFace(GL_BACK); // возвращаем в первоначальное состояние

В нашем примере это превосходно работает для кубиков, но не будет работать для пола, так как отсечение фронтальных полигонов полностью уберёт пол. Это решает проблему с эффектом Питера Пена, но только для объектов, которые имеют поверхность со всех сторон. Если вы собираетесь использовать этот способ, используйте отсечение фронмально ориентированных полигонов только там, где это имеет смысл.

пер.- конкретно в этом примере нет ничего страшного в том, что отсечение полностью уберёт пол, так как ниже него нет объектов, и не важно, есть под ним тень или нет.) (Прим.

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

Это происходит из-за того, что для удалённой точки в системе отсчёта источника света координаты будут больше, чем 1. Ещё один видимый недостаток, который вам может нравиться или нет — некоторые поверхности за пределами области видимости источника света могут рисоваться затенёнными, даже если на них теоретически должен падать свет. 0 до 1. 0, а текстурные координаты меняются только от 0. Если поверхность находится слишком в стороне от источника света, то в карте глубины нет значения для неё. 0.

shadow_mapping_outside_frustum

Освещённая область показывает, как карта глубины проецируется на пол. На изображении выше можно представить область света — всё остальное находится в тени. Причина такого поведения в том, что ранее для текстуры глубины мы установили режим GL_REPEAT.

0 — это значит, что они никогда не будут в тени (так как у видимого объекта глубина не может быть больше единицы). Хотелось бы для таких фрагментов возвращать глубину 1. Для этого мы установим цвет края для текстуры и остановим опцию "GL_CLAMP_TO_BORDER"

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
float borderColor[] = ;
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

0, из за чего значение shadow в шейдере будет 0. Теперь, если мы читаем значение из карты глубины по координатам вне интервала [0,1], мы будем получать в ответ глубину 1. Теперь сцена выглядит лучше: 0.

shadow_mapping_clamp_edge

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

0, в этом случае опция GL_CLAMP_TO_BORDER нам не помогает. Для таких областей координата z в пространстве источника света оказывается больше, чем 1. 0) со значением из карты глубин (в текстуре глубины хранятся значения не больше 1. Мы сравниваем реальную глубину (которая больше 1. 0) — поэтому далёкие участки будут в тени.

Исправление этого эффекта относительно простое — мы будем считать незатенёнными все объекты, для которых z координата больше единицы.

float ShadowCalculation(vec4 fragPosLightSpace)
{ [...] if(projCoords.z > 1.0) shadow = 0.0; return shadow;
}

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

shadow_mapping_over_sampling_fixed

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

PCF

Percentage-closer filtering

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

shadow_mapping_zoom.png

В итоге получаются зазубренные края тени. Текстура глубины имеет фиксированный размер, и довольно часто несколько пикселей на итоговой картинке используют значение глубины из одного и того же текселя текстуры.

Вы можете уменьшить размер этих квадратиков, если увеличите размер текстуры с тенями и подберёте положение источника света.

Идея состоит в том, чтобы выбирать несколько значений из карты глубины — каждое с немного отличающимися координатами. Ещё одно (частичное) решение проблемы с зазубренными краями теней — PCF (Percentage-closer filtering), содержащий различные функции фильтрации, которые дают нам мягкие тени. Потом усредняем все результаты и получаем красивые мягие тени. Для каждого значения мы проверяем, находимся в тени или нет.

Простая реализация PCF — просто выбрать соседние тексели в карте глубины и усреднить результат:

float shadow = 0.0;
vec2 texelSize = 1.0 / textureSize(shadowMap, 0);
for(int x = -1; x <= 1; ++x)
{ for(int y = -1; y <= 1; ++y) { float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r; shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0; }
}
shadow /= 9.0;

Единица, делённая на этот вектор, даёт размер текселя на текстуре. Здесь textureSize — вектор из двух чисел, ширины и высоты текстуры глубины. В этом примере мы берём 9 значений в окрестности точки (x,y), проверяем на наличие тени и в конце делим на количество проверок, получая усреднённое значение. Мы будем смещаться на этот размер, чтобы читать значения из соседних текселей.

Ниже картинка с тенями с простым PCF фильтром: Если использовать больше точек и/или изменить texelSize, то можно улучшить качество мягких теней.

shadow_mapping_soft_shadows

У них нет резких краёв, но если приблизиться, то всё ещё видно артефакты (впрочем, 9 оттенков тени разнообразнее одного). Издалека тени выглядят намного лучше. В целом, PCF даёт хороший результат в большинстве случаев.

Исходный код примера

Но эта статья и так уже большая, так что обсудим это позже. На самом деле, существует намного больше способов реализовать PCF, и некоторые методы могут значительно улучшить мягкие тени.

Перспективная проекция vs ортографическая

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

Перспективную проекцию чаще всего используют для точечных источников света, а ортографическую — для направленного света. shadow_mapping_projection
Для перспективной проекции позиция источника света реально важна (в отличие от источника направленного освещения).

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

#version 330 core
out vec4 FragColor; in vec2 TexCoords; uniform sampler2D depthMap;
uniform float near_plane;
uniform float far_plane; float LinearizeDepth(float depth)
{ float z = depth * 2.0 - 1.0; // Back to NDC return (2.0 * near_plane * far_plane) / (far_plane + near_plane - z * (far_plane - near_plane));
} void main()
{ float depthValue = texture(depthMap, TexCoords).r; FragColor = vec4(vec3(LinearizeDepth(depthValue) / far_plane), 1.0); // perspective // FragColor = vec4(vec3(depthValue), 1.0); // orthographic
}

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

Дополнительные ресурсы:

Оригинальная статья

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»