В уроке 24 мы видели, как реализованы тени с помощью техники Карта Теней. Тени, полученные в результате, ущербны, им не хватает сглаживания; это хорошо заметно на следующем изображении:
Этот урок расскажет об методе (одном из множества) решения этой проблемы. Он назван Percentage Closer Filtering, или PCF. Идея в выборке из карты теней вокруг текущего пикселя и сравнении его глубины с этой выборкой. Получив среднее значение мы можем добиться гладкого перехода между светом и тенью. Например, посмотрим на следующую карту теней:
Каждая ячейка хранит значение глубины для каждого пикселя (когда смотрим с позиции источника света). Для простоты представим, что глубина всех пикселей выше 0.5 (когда смотрим с позиции камеры). Согласно методу из урока 24, все пиксели, чье значение в карте теней меньше чем 0.5 считаются в тени, а те, чье значение больше либо равно 0.5 будут на свету. Это дает грубую границу между светом и тенью.
Теперь сосредоточимся на следующем - близкие к границе между светом и тенью пиксели окружены пикселями, чье значение на карте теней больше чем 0.5, и пикселями, чье значение меньше 0.5. Если мы возьмем этих соседей и вычислим средний результат, то получим значение, которое поможет нам сгладить границу между светом и тенью. Конечно, мы не знаем, какие пиксели близки к границе, поэтому выборка будет происходить для всех. В этом вся суть. В этом уроке мы берем 9 пикселей в квадрате 3 на 3 вокруг каждого пикселя и находим среднее значение. Это и будет теневой порог в отличии от 0.5 или 1.0, чем мы пользовались в уроке 24.
Давайте теперь рассмотрим исходный код, который реализует PCF. Для этого мы пройдемся по изменениям в коде урока 24. Возможно вам придется сделать небольшое ревью того урока для освежения памяти.
lighting.glsl:80
uniform sampler2DShadow gShadowMap;
#define EPSILON 0.00001
float CalcShadowFactor(vec4 LightSpacePos)
{
vec3 ProjCoords = LightSpacePos.xyz / LightSpacePos.w;
vec2 UVCoords;
UVCoords.x = 0.5 * ProjCoords.x + 0.5;
UVCoords.y = 0.5 * ProjCoords.y + 0.5;
float z = 0.5 * ProjCoords.z + 0.5;
float xOffset = 1.0/gMapSize.x;
float yOffset = 1.0/gMapSize.y;
float Factor = 0.0;
for (int y = -1 ; y <= 1 ; y++) {
for (int x = -1 ; x <= 1 ; x++) {
vec2 Offsets = vec2(x * xOffset, y * yOffset);
vec3 UVC = vec3(UVCoords + Offsets, z + EPSILON);
Factor += texture(gShadowMap, UVC);
}
}
return (0.5 + (Factor / 18.0));
}
Это обновленная функция вычисления порога теней. Она начинается с того, что мы вручную производим деление перспективы на координаты пространства клиппера с позиции источника света, а затем преобразования из отрезка (-1,+1) в (0,1). Теперь у нас есть координаты, которые мы можем использовать для выборки из карты теней и Z значение для сравнения с результатом выборки. Дальше все немного по-другому. Мы собираемся выбрать квадрат 3 на 3, для этого нам потребуются всего 9 координат текстуры. Координаты должны соответствовать выборке текселя ровно на единичном интервале по осям X и/или Y. Поскольку UV координаты текстуры идут от 0 до 1 и отображаются в отрезок текселей (0, Ширина-1) и (0, Высота-1), соответственно, мы делим 1 на ширину и высоту текстуры. Эти значения хранятся в uniform-переменной gMapSize (подробности в исходном коде). Это дает нам смещение в пространстве координат текстуры между 2 соседними текселями.
Далее идет вложенный цикл for и вычисления вектора смещения для каждого из 9 текселей, которые мы собираемся получить. Несколько последних строк внутри цикла возможно выглядят немного странно. Мы делаем выборку из карты теней используя вектор с 3 компонентами (UVC) вместо 2-х. Последний компонент хранит значение, которое мы использовали в уроке 24 для ручного сравнения со значением из карты теней (глубина источника Z плюс небольшое отклонение для избежания Z-поединка). Отличие в том, что мы используем sampler2DShadow как тип 'gShadowMap' вместо sampler2D. Когда выборка делается из типа для теней (sampler1DShadow, sampler2DShadow, т.д.) GPU производит сравнение между значением текселя и величиной, которую мы поставляем как последний компонент вектора координат текстуры (второй компонент для 1D, третий для 2D и т.д.). Мы получим 0, если они не равны и 1, если равны. Тип сравнения задается через GL API, а не через GLSL. Это изменение будет указанно далее. Теперь, предположим, что мы получили 0 как результат для тени и 1 для света. Мы суммируем 9 результатов и делим их на 18. Таким образом мы получаем значение между 0 и 0.5. Мы добавляем его к базовым 0.5 и получаем теневой коэффициент.
shadow_map_fbo.cpp:39
bool ShadowMapFBO::Init(unsigned int WindowWidth, unsigned int WindowHeight)
{
// Создаем FBO
glGenFramebuffers(1, &m_fbo);
// Создаем буфер глубины
glGenTextures(1, &m_shadowMap);
glBindTexture(GL_TEXTURE_2D, m_shadowMap);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32,
WindowWidth, WindowHeight, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
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_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_shadowMap, 0);
// Отключаем запись в буфер цвета
glDrawBuffer(GL_NONE);
// Отключаем чтение из буфера цвета
glReadBuffer(GL_NONE);
GLenum Status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (Status != GL_FRAMEBUFFER_COMPLETE) {
printf("FB error, status: 0x%x\n", Status);
return false;
}
return true;
}
Вот как мы настраиваем нашу текстуру карты теней для работы с теневой выборкой в шейдере вместо обычной выборки. Добавлены 2 строки кода, они выделены отступом. Сначала мы устанавливаем сравнение текстуры в режим 'сравнить ссылку с текстурой'. Единственное доступное значение для третьего параметра это GL_NONE, которое является значением по-умолчанию и задает обычное поведение при выборке, как для не теневых текстур. Второй вызов glTexParameteri устанавливает функцию сравнения в режим 'меньше чем либо равно'. Это значит, что результат операции выборки будет 1.0 если значение по ссылке меньше либо равно значению в текстуре, а иначе 0. Вы можете использовать GL_GEQUAL, GL_LESS, GL_GREATER, GL_EQUAL, GL_NOTEQUAL для аналогичных типов сравнения. Ну вы поняли. Также доступны GL_ALWAYS, который всегда возвращает 1.0 и GL_NEVER, который всегда возвращает 0.0.
tutorial42.cpp:174
void ShadowMapPass()
{
glCullFace(GL_FRONT);
...
}
void RenderPass()
{
glCullFace(GL_BACK);
...
}
Последнее, что я бы хотел отметить - небольшое изменение для избежания самозатенения. Самозатенение - большая проблема при работе почти со всеми техниками теней, и причина тому - точность буфера глубины сильно ограничена (даже на 32 битах). Проблема касается полигонов, которые расположенны на свету и не в тени. В проходе карты теней мы рендерим глубину в карту теней, а в проходе рендера мы сравниваем глубину между значениями в карте теней. Из-за проблем точности мы часто получаем Z поединок, что приводит к тому, что некоторые пиксели в тени, хотя остальные на свету. Для решения этой проблемы мы обращаем обрезание таким образом, что обрезаем лицевую сторону полигона в проходе карты теней (и рендерим только обратную сторону полигонов на карту теней). В проходе рендера мы возвращаемся к обычному обрезанию. Поскольку в реальном мире окклюдеры полностью замкнуты, то никакой проблемы с использованием обратной стороны полигона для сравнения глубины вместо лицевой. Вам следует отключить код выше самим убедиться в результате.
После применения всех изменений наши тени выгледят как-то так: