Percentage-Closer Filtering (ru)
В комментариях к предыдущему реверсу, товарищ uncle_lag
попросил меня поподробнее рассказать, что такое PCF. Я так увлёкся, что решил, что данная тема достойна отдельного постинга.
Для смягчения тени, мы не можем блурить shadow map (в привычном понимании этого слова), потому что в ней содержится глубина пикселя с позиции источника света. Операция разблуривания (усреднения соседних значений) глубины не имеет смысла. PCF алгоритм заключается в том, чтобы вместо того, чтобы проверить 1 пиксель на затенённость, проверяется несколько пикселей по заранее заданному filter kernel. Затем полученные значения (1 - в тени, 0 - не в тени) усредняются. Т.о. если в тени оказались всего 3 пикселя из 4-х, то степень затенённости можно установить в 0.75 (либо 75%), что создаёт полутень (переход от затенённой области в светлую). Данный алгоритм, в варианте квадратной области размером 2х2, реализован аппаратно в NVidia hardware, как составная часть т.н. NV hardware shadowmap hack, начиная с GeForce 3. Можно отметить, что для 2х2 алгоритма мы сможем иметь на выходе, только 5 градаций интенсивности затенения (0, 0.25, 0.5, 0.75 и 1), что приведет к довольно заметной "блочной" структуре полутени.
Тень без PCF
Тень с 2x2 PCF без линейной интерполяции
Для этого на NV железе выполняется ещё и линейная интерполяция полученных значений по точному положению экранного пикселя, в пространстве карты тени. Алгоритм достаточно прост. Post-perspective XY координаты текущего пикселя в пространстве SM имеют координаты [0..1]. Домножая их на размер карты тени, в целой части результата мы получим координаты текселя карты тени соответсвующего данному экранному пикселю. В дробной же части у нас будет субтексельное положение пикселя внутри текселя sm.
Пояснительная картиночка:
На ней изображён маппинг одного shadow texel в 3 screen pixels. Этот случай является как раз тем пресловутым perspective aliasing'ом с которым более-менее успешно борются все перспективные техники (PSM, TSM, LisPSM и др). Но речь сейчас не об этом. В нашем примере, координаты каждого из 3-х пикселей, будут иметь одно и то же целочисленное значение в пространстве карты теней. Но дробная часть координаты будет отличаться. Она то и является той субпискельной точностью, которая поможет нам ещё более сгладить края затенённых областей. Для первого сверху экранного пикселя она будет примерно 0.8, для среднего 0.5, а для нижнего 0.2.
Теперь рассмотрим вид с позиции карты тени (увеличенная 2х2 область):
Синим кружочком показано субтексельное положений экранного пикселя на карте тени. Цифрами - результат теста глубины из sm, c глубиной тестируемого экранного пикселя (1 - в тени, 0 - нет). Возьмём верхних 2 текселя shadow map'ы. При подходе влоб (без учёта субтексельного положения), степень затенённости в результате будет 0.5 ((1 + 0) / 2). Но мы знаем, что субтексельная позиция пикселя по оси u в нашем случае равна 0.2 (примерно). Следовательно, вместо простого усреднения соседних значений, мы можем сделать их линейную интерполяцию по субтексельному положению. Т.о. степень затенённости в нашем случае будет x + s(y - x) = 1 + 0.2(0 - 1) = 0.8, двигаясь дальше, вдоль оси u мы будем получать всё меньшее значение затенённости пикселя, т.о получая плавный градиент затенения. Тоже самое проделывается и по оси v.
Именно таким образом инженеры NV реализовали этот алгоритм в железе. Результат налицо:
Фрагмент кода шейдера для эмуляции поведения NV видеокарт на ATI:
// Perform Percentage Closer Filtering
// First determine lerp amounts
float4 la;
float2 texLA = frac(fragPos.xy * shadowMapSize.xy);
// Gather samples
// Fetch 4 neighbour points
depth.x = tex2D(shadow_s, fragPos).r;
depth.y = tex2D(shadow_s, fragPos + float4(shadowMapSize.z, 0.0, 0.0, 0.0)).r;
depth.z = tex2D(shadow_s, fragPos + float4(0.0, shadowMapSize.w, 0.0, 0.0)).r;
depth.w = tex2D(shadow_s, fragPos + float4(shadowMapSize.z, shadowMapSize.w, 0.0, 0.0)).r;
// Check visibility for all points
depth = depth < fragPos.zzzz ? 0.0 : 1.0;
// Final shadow factor
return lerp
(
lerp(depth.x, depth.y, texLA.x),
lerp(depth.z, depth.w, texLA.x),
texLA.y
);
Вопросы?