sunLoadingImage
whowedImag
decoration left 1
decoration left 2
transhome
transprojects
transgallery
transarticles
decoration rigth
English
Українська
Show/Hide search bar
black cat logo variable logo
[13 Гру 2012]

Покращення для shadow mapping в OpenGL і GLSL

У цьому уроці показано як покращити результати метода побудови тіней shadow mapping, основні проблеми та недоліки shadow mapping, а також можливі методи їх вирішення. Серед розглянутих методів покращення: Variance Shadow Mapping (VSM), Exponential Shadow Maps (ESM), Percentage Closer Filtering (PCF), Stratified Poisson Sampling, Rotated Poisson Sampling та ін. Метод Shadow Mapping та метод shadow mapping для точкових джерел світла описаний у попередніх уроках.
Проблеми shadow mapping
Базова версія shadow mapping є простою в імплементації, але потребує багато часу на налаштування та вдосконалення. Також проблемою є те, що параметри для shadow mapping повинні вибиратися в залежності від сцени. Серед проблем shadow mapping:
  • Z-fighting - артефакти на освітленій стороні поверхонь, які виникають через помилки у порівнянні значень відстаней з shadow map і поточною відстанню фрагмента до джерела світла. В більшості випадків ця проблема вирішується через додатковий зсув для відстаней в shadow map.
  • Aliasing - видимий аліасинг при переході від світла до тіні. Характеризується видимими "східцями". Аліасинг виникає через занадто малий розмір shadow map. Багатьом фрагментам, які видно через камеру, відповідає один тексель в shadow map. Проблема вирішується через підбір оптимального розширення shadow map. Але при зміні точки погляду (приближені), знову буде видно аліасинг. Найбільші проблеми з аліасингом можуть виникнути, коли промені світла є паралельні поверхні.
  • Перспективний aliasing - збільшення аліасингу (східчатості) починаючи від позиції джерела світла до дальніх меж, для яких розраховується shadow map. Виникає через збільшення відношення кількості видимих камері фрагментів до кількості текселів в shadow map.
  • Lost detail - тіні, які не видображаються чи хаотично з'являються. Ця помилка виникає, коли shadow map є дуже великою, а об'єкт, що відкидає тінь є дуже малим (у фрагментах). Потрібно зменшити shadow map до оптимального розміру.
  • Banding - смуги на границях світла і тіні. Виникають через недостатню кількість і випадковість вибірок при розмитті країв тіней.
  • Peter Panning - ефект Пітера Пена. Тінь занадто зсунута, і в деяких випадках може здатися, що об'єкт висить в повітрі. Артефакт виникає через занадто великий зсув відстаней (який використовується для подолання z-fighting).
  • Z-fighting - помилки в порівнянні відстаней
    Peter Panning - через занадто великий зсув відстаней обєкти можуть здаватися такими, що знаходяться в повітрі
    Aliasing - мале відношення текселів в shadow map до кількості фрагментів видимих у камеру
    Banding виникає при створені розмитих тіней з недостатньою кількістю і випадковістю вибірок
    Далі розглянуто методи вирішення проблем та покращення результатів shadow mapping.
    Поверхні, які не орієнтовані до світла є в тіні
    Однією з найпростіших оптимізацій метода shadow mapping є автоматичне затінення тих фрагментів, напрямок нормалей в яких є протилежним напрямку від фрагмента до світла. Перевірка орієнтації виконується через значення скалярного добутку векторів нормалі (N) і вектора від світла (L). Якщо скалярний добуток менше нуля, то фрагмент у тіні. Так як потоки на GPU обробляють фрагменти квадратними групами, то в випадках, коли обробляються поверхні орієнтовані в протилежну до світла сторону, буде виконуватися тільки одна коротка гілка виконання шейдера. На границях світла і тіні, а також на поверхнях орієнтованих до світла, час виконання шейдера завжди буде рівним довшій гілці виконання. Нормалі об'єктів повинні бути правильними для коректної роботи цієї оптимізації. Ця оптимізація може погіршити якість тіней при розмиванні. Також ліквідувується артефакт типу Peter Panning на поверхнях, які напрямлені до світла. В наступному коді наведено приклад перевірки орієнтації поверхні до світла:
    // признак затіненості. Може бути не 0, а менше число
    // для покращення можливого розмиття границь тіней
    if(dot(N, L) < 0)
    {
       shadowFactor = 0;
    }
    else{ ... }
    Застосування перевірки на орієнтацію поверхні до світла
    Додатковий зсув для відстаней в shadowMap
    Додатковий зсув для відстаней в shadow map використовується для того, щоб мінімізувати z-fighting. Розглянемо наступне зображення. Поверхня об'єкта показана зеленою рискою, shadow map - товста чорна риска, червоними лініями показано проекцію текселів з shadow map на поверхню, чорними точками - значення, які зберігаються в shadow map. На малюнку зліва не застосований додатковий зсув відстані. Вся поверхня повинна бути освітленою, але показано (чорною лінією від камери), що частина фрагментів, які проектуються на перший тексель shadow map є в тіні, так як їхня відстань до світла є більшою, ніж відстань записана в shadow map. Інша частина фрагментів, що відповідає цьому ж текселю в shadow map є освітленою. Ситуація аналогічна і для наступних текселів: половина відповідних фрагментів буде затінена, половина - освітлена. Через це і виникає z-fighting, показаний на одному з зображень на початку уроку. На зображені справа, до відстаней, що зберігаються в shadow map доданий зсув. Чорними точками позначено нові значення відстані, що відповідають текселям в shadow map. Тепер усі фрагменти освітлені.
    Вміст shadow map без додаткового зсуву відстаней (зліва) і зі зсувом (справа)
    Проблемою є те, щоб визначити оптимальний зсув для кожного текселя shadow map. Якщо зробити занадто малий зсув, то збережеться z-fighting. Якщо зсув буде занадто великим, то помітним стане атрефакт Peter Paning. Зсув повинен залежати від точності карти тіней, а також від нахилу поверхні відносно напрямку променів світла.
    Зсув можна можна зробити вручну. Наприклад, при записі відстаней в shadow map, до кожного значення додати малий доданій зсув epsilon.
    float distToLight = distance(fragmentWorldPosition, lightPosition) + epsilon;
    Такого зсуву може бути недостатньо, так як він не бере до уваги нахил поверхні. Розглянемо наступне зображення. Зліва показано збережені відстані без додаткового зсуву. Якщо поверхня перпендикулярна променям світла, то зсув повинен бути мінімальним. Якщо поверхня паралельна променям світла, то потрібно збільшити зсув. Праворуч показано, що для кожного текселя було додано різний розмір зсуву, залежно від нахилу поверхні. Розмір такого нахилу можна порахувати в залежності від нормалі в фрагменті і напрямку до світла. Приклад показано в наступному коді:
    Зсув повинен залежати від нахилу поверхні
    // скалярний добуток повертає косинус кута між N та L в межах [-1, 1]
    // переводимо в межі [0, 1] і інвертуємо
    float offsetMod = 1.0 - clamp(dot(N, L), 0, 1)
    float offset = minOffset + maxSlopeOffset * offsetMod;

    // можливий інший розрахунок модифікатора зсуву
    // дає дуже великий сзув для поверхонь паралельних променям світла
    float offsetMod2 = tan(acos(dot(N, L)))
    float offset2 = minOffset + clamp(offsetMod2, 0, maxSlopeOffset);
    OpenGL може автоматично розрахувати і додати зсув до значень, які записуються у Z-Buffer. Налаштування зсуву здійснюється через функцію glPolygonOffset. Можна встановити два параметри для зсуву: множник для зсуву в залежності від нахилу поверхні, і множник, який визначає кількість необхідних мінімально можливих зсувів (в залежності від формату shadow map).
    glEnable(GL_POLYGON_OFFSET_FILL);
    glPolygonOffset(1.0f, 1.0f);
    Автоматичне порівняння відстаней з лінійним фільтром вибірки
    OpenGL дозволяє проводити вибірку з shadow map з врахуванням лінійного фільтру вибірки. Лінійний фільтр вибірки повертає інтерпольоване значення з чотирьох найближчих текселів від місця вибірки, замість значення одного текселя. Але для shadow map проводиться не інтерполяція відстаней. Для кожного з чотирьох текселів проводиться автоматичне порівняння з відстанню поточного фрагмента. Якщо тест проходить перевірку, тобто відстань до фрагмента є меншою за значення текселя, то до результату, що повертається, додається 0.25. Якщо перевірка не пройшла - результат не змінюється.
    Автоматичне порівняння відстаней з лінійним фільтром вибірки
    Якщо не пройшло жодної перевірки, то повертається 0. Якщо усі, то 1. Тобто повернене значення можна інтерпретувати як фактор затіненості. Це лінійна фільтрація не відстаней, а результатів порівняння відстаней. Для правильної роботи автоматичних порівнянь, в шейдері повинен бути використаний тип семплера - samplerShadow. Наступний фрагмент коду показує, як потрібно ініціалізувати текстуру для автоматичної перевірки з врахуванням лінійного фільтру вибірки:
    // привязка shadow map
    glBindTexture(GL_TEXTURE_2D, shadowMap);

    // встановлення лінійних фільтрів для семплінгу
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_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);
    Порівняння відстанні фрагмента з чотирма значенями з shadow map дозволяє зробити перехід від світла до тіні більш розмитим, а також зменшує видимий аліасинг. В залежності від драйвера, одна вибірка з лінійним фільтром може бути набагато швидша ніж чотири окремі вибірки і порівняння.
    Percentage closer filtering
    Percentage closer filtering (PCF) - найпростіший метод створення м'яких тіней в shadow mapping. Замість вибірки з одного текселя shadow map, проводяться багато вибірок. Фактор затінення розраховується як відношення вдалих порівнянь до загальної кількості порівнянь. Зазвичай Percentage closer filtering також використовує вибірку з лінійним фільтром. Тобто якщо PCF робить чотири вибірки, то насправді в shadow map порівнюється 16 значень. Вибірки з допомогою PCF повинні бути зроблені через текстурні координати, які дозволяють провести унікальні вибірки. Тобто, щоб автоматична лінійна вибірка не проводила повторних вибірок з тих самих текселів shadow map.
    PCF з 4 вибірками. З лінійним фільтром це врахування 16 текселів з shadow map
    Якість розмиття тіней в Percentage Closer Filtering напряму залежить від кількості вибірок з shadow map. Мала кількість вибірок і великий радіус PCF призводить до появи смуг (Banding) вздовж межі світла і тіней. Чим більше унікальних вибірок, тим якісніші "м'які" тіні. При цьому велика кількість вибірок призводить до погіршення швидкодії шейдера. Великий радіус для фільтра вимагає використання більшого зсуву для боротьби з z-fighting. В наступному фрагменті коду показано, як імплементується Percentage Closer Filtering:
    PCF з 16 вибірками на shadow maps різного розміру. З лінійним фільтром це врахування 64 текселів з shadow map
    // кількість вибірок
    const int numSamplingPositions = 4;

    // зсуви вибірок
    vec2 kernel[4] = vec2[]
    (
       vec2(1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0), vec2(1.0, -1.0)
    );

    // функція, яка проводить вибірку і акумулює фактор затінення
    void sample(in vec3 coords, in vec2 offset,
                inout float factor, inout float numSamplesUsed)
    {
       factor += texture(
             u_textureShadowMap,
             vec3(coords.xy + offset, coords.z)
          );
       numSamplesUsed += 1;
    }

    void main()
    {
       // ...

       // проведення вибірки для кожного текселя з набору PCF зсувів
       float shadowFactor = 0.0;
       float numSamplesUsed = 0.0;
       float PCFRadius = 1;
       for(int i=0; i<numSamplingPositions; i++)
       {
          sample(projectedCoords,
                kernel[i] * shadowMapStep * PCFRadius,
                shadowFactor,
                numSamplesUsed
             );
       }

       // зважити фактор затінення
       shadowFactor = shadowFactor/numSamplesUsed;
    }
    Вибірка через диск Пуасона (Poisson sampling kernel)
    Диск Пуасона дозволяє проводити семплінг через більш нерегулярну структуру зсувів, ніж в простому методі Percentage closer filtering. Зсуви в диску Пуасона є рівномірно розміщеними на площині і не розміщені ближче один до одного, ніж певна задана відстань. Цей факт дозволяє в шейдері при достатньому радіусі вибірок уникнути повторного семплінгу тих самих текселів з shadow map. Щоб згенерувати зсуви для ядра Пуасона можна використати програму Poisson Disk Generator. Причиною використання диска Пуасона є те, що усі точки розміщені випадково і рівномірно. Цього важко досягти генеруючи зсуви випадково.
    У порівнянні з однорідною сіткою зсувів для PCF, семплінг через диск Пуасона з меншою кількістю вибірок може дати трошки кращі результати: зменшити Banding та зберегти рівень розмиття. На зображеннях показано приклад тіней, які утворилися з використанням диска Пуасона для семплінгу, а також наведено розміщення вибірок.
    PCF з ядром Пуасона на 9 вибіркок. З лінійним фільтром це врахування 36 текселів з shadow map
    Диск Пуасона з девятьма зсувами
    Випадковий поворот ядра вибірок (Rotated Poisson kernel)
    У будь-якому випадку, якщо використовувати рівномірну сітку вибірок, чи диск Пуасона для PCF, виникає проблема між достатньою якістю розмиття тіней та швидкодією. Хороше розмиття і відсутність Banding завжди потребують великої кількості вибірок, що однозначно зменшать швидкодію. Причиною Banding є те, що для кожного фрагмента з shadow map вибірки проводяться через таке саме ядро зсувів, і через це виникають видимі смуги (рівні) вздовж границь тіней.
    Можна спробувати випадковим чином генерувати місця вибірок для кожного фрагмента. Як писалося раніше, випадкові вибірки можуть призводити до багаторазового семплінгу з тих самих текселів. Потрібно згенерувати диск Пуасона. Але недоречно кожного разу розраховувати диск Пуасона, так як це може дуже зменшити швидкодію. Замість цього можна випадковим чином повертати диск Пуасона на випадковий кут для кожного фрагмента.
    Виникає інша необхідність - згенерувати випадковий кут у шейдері. Зазвичай у шейдері є недоступним генератор псевдовипадкових чисел. Тож потрібно його реалізувати самостійно. Для того, щоб після кожного рендера у сцені не змінювалися випадковим чином згенеровані числа, генератор псевдовипадкових чисел повинен залежати від позиції фрагмента у сцені. Генератор можна написати повністю у шейдері, а можна зберегти випадкові числа в текстурі, і проводити вибірку випадкових чисел з неї (використавши позицію в сцені, як текстурні координати). Приклад генератора псевдовипадкових чисел наведений в наступному коді:
    9 вибірок з випадково повернутим диском Пуасона
    // генерує псевдовипадкове число в межах [0, 1]
    // seed - світові координати фрагмента
    // freq - модифікатор для seed. Чим більший, тим швидше
    // будуть змінюватися псевдовипадкові числа зі зміною позиції

    float random(in vec3 seed, in float freq)
    {
       // project seed on random constant vector
       float dt = dot(floor(seed * freq), vec3(53.1215, 21.1352, 9.1322));
       // return only fractional part
       return fract(sin(dt) * 2105.2354);
    }
    Так як нам потрібно повертати диск Пуасона через випадкові кути, то замість збереження в текстурі випадкових чисел, можна одразу зберегти cos(theta) та sin(theta), які використовуються при двовимірному повороті.
    Далі в коді наведено реалізацію Rotated Poisoon Kernel без використання текстур. На зображеннях наведено результати для різного радіусу диска Пуасона при однаковій кількості вибірок. Як видно з зображень, даний метод повністю забирає Banding, збільшує можливий радіус розмиття, але при малій кількості вибірок виникає шум (noise).
    9 вибірок з випадково повернутим диском Пуасона і великим радіусом диску
    // диск Пуасона зі зсувами
    const int numSamplingPositions = 9;
    uniform vec2 kernel[9] = vec2[]
    (
       vec2(0.95581, -0.18159), vec2(0.50147, -0.35807), vec2(0.69607, 0.35559),
       vec2(-0.0036825, -0.59150), vec2(0.15930, 0.089750), vec2(-0.65031, 0.058189),
       vec2(0.11915, 0.78449), vec2(-0.34296, 0.51575), vec2(-0.60380, -0.41527)
    );

    // повертає випадковий кут
    float randomAngle(in vec3 seed, in float freq)
    {
       return random(seed, freq) * 6.283285;
    }

    void main()
    {
       // ...

       // згенерувати випадковий кут повороту для кожного фрагмента
       float angle = randomAngle(o_worldPos, 15);
       float s = sin(angle);
       float c = cos(angle);
       float PCFRadius = 20;
       for(int i=4; i < numSamplingPositions; i++)
       {
          // поворот зсуву
          vec2 rotatedOffset = vec2(kernel[i].x * c + kernel[i].y * s,
             kernel[i].x * -s + kernel[i].y * c);
          sample(projectedCoords, rotatedOffset * shadowMapStep * PCFRadius,
             shadowFactor, numSamplesUsed);
       }
       shadowFactor = shadowFactor/numSamplesUsed;
    }
    Випадковий вибір вибірок (Stratified Poisson kernel)
    Замість повороту диску Пуасона можна зберегти більшу кількість зсувів диска Пуасона, і для кожного фрагмента використовувати тільки випадково вибрані зсуви. Це призведе до того, що будуть використовуватися різні текселі з shadow map. Як і в випадку з поворотом диска Пуасона, цей метод зменшить Banding і збільшить розмиття. Але шум при такому підході більший. Також випадково вибрані вибірки можуть бути однаковими.
    4 випадкові вибірки через Stratified Poisson kernel
    // диск Пуасона для випадкових вибірок
    const int numSamplingPositions = 9;
    uniform vec2 kernel[9] = vec2[]
    (
       vec2(0.95581, -0.18159), vec2(0.50147, -0.35807), vec2(0.69607, 0.35559),
       vec2(-0.003682, -0.5915), vec2(0.1593, 0.08975), vec2(-0.6503, 0.05818),
       vec2(0.11915, 0.78449), vec2(-0.34296, 0.51575), vec2(-0.6038, -0.41527)
    );

    // повертає псевдовипадкове число в межах [0, maxInt)
    int randomInt(in vec3 seed, in float freq, in int maxInt)
    {
       return int(float(maxInt) * random(seed, freq)) % maxInt;
    }

    void main()
    {
       // ...

       // чотири випадкові вибірки
       float radius = 5;
       for(int i=1; i <= 4; i++)
       {
          // випадковий індекс, який залежить від позиції фрагмента
          int randomIndex = randomInt(o_worldPos * i, 100, numSamplingPositions);
          // вибірка через випадковий індекс
          sample(projectedCoords, kernel[randomIndex] * shadowMapStep * radius,
                shadowFactor, numSamplesUsed);
       }
       shadowFactor = shadowFactor/numSamplesUsed;
    }
    Раннє визначення затіненості (Early bailing)
    Метод ранього визначення затіненості є оптимізацією, яка повинна збільшити швидкодію у сценах, де є великі частини сцени, які не затінені, чи які затінені (тобто переходи між тіню і світлом не є частими, наприклад, не такими як тіні від дерева з малою кількістю листя). Метод полягає в тому, щоб спочатку зробити певну кількість доволі віддалених вибірок з shadow map. Якщо усі вибірки показали, що фрагмент в тіні, то припиняється виконання, і рахується, що фрагмент у тіні. Аналогічно, якщо усі вибірки показали, що фрагмент освітлений, то припиняється подальше виконання, і рахується, що фрагмент повністю освітлений.
    Артефакт :), що виникає при різких переходах від тіні до світла, і знову до тіні при використані Early Bailing (якщо радіус диска занадто великий)
    Інакше, проводяться подальші вибірки з shadow map. Може виконуватися довільна кількість вибірок довільним методом. При великій відстані між початковими текселями може статися так, що малі ділянки світла будуть в тіні, а малі ділянки тіні - у світлі. На зображенні наведено результати роботи Early bailing з таким артефактом.
    // диск Пуасона з граничними зсувами
    const int numSamplingPositions = 13;
    uniform vec2 kernel[13] = vec2[]
    (
       vec2(-0.9328896, -0.03145855), // лівий
       vec2(0.8162807, -0.05964844), // привий
       vec2(-0.184551, 0.9722522), // верхній
       vec2(0.04031969, -0.8589798), // нижній
       vec2(-0.54316, 0.21186), vec2(-0.039245, -0.34345), vec2(0.076953, 0.40667),
       vec2(-0.66378, -0.54068), vec2(-0.54130, 0.66730), vec2(0.69301, 0.46990),
       vec2(0.37228, 0.038106), vec2(0.28597, 0.80228), vec2(0.44801, -0.43844)
    );

    void main() { // ...

       float PCFRadius = 5;

       // провести чотири граничні вибірки
       for(int i=0; i < 4; i++)
       {
          sample(projectedCoords, kernel[i] * shadowMapStep * PCFRadius,
             shadowFactor, numSamplesUsed);
       }

       // перевірка на раннє визначення затіненості
       if(shadowFactor>0.1 && shadowFactor<3.9)
       {
          // додатковий семплінг, наприклад, з використанням випадкового повороту
          float angle = randomAngle(o_worldPos, 15);
          float s = sin(angle);
          float c = cos(angle);
          // від четвертого зсуву і до останього
          for(int i=4; i < numSamplingPositions; i++)
          {
             vec2 rotatedOffset = vec2(kernel[i].x * c + kernel[i].y * s,
                kernel[i].x * -s + kernel[i].y * c);
             sample(projectedCoords, rotatedOffset * shadowMapStep * PCFRadius,
                shadowFactor, numSamplesUsed);
          }
       }
    }
    Variance shadow mapping (VSM)
    Основною проблемою розглянутих вище методів Shadow Mapping є необхідність багатьох вибірок з shadow map і порівнянь з поточною відстанню для того, щоб розмити краї тіней. Було б добре розмити shadow map, використати автоматичну фільтрацію, провести тільки одну вибірку і отримати плавний перехід від світла до тіні. Але в стандартному shadow mapping цього зробити не можна, так як вибірка з shadow map і наступне порівняння не є лінійною операцією. Це призведе до неправильних тіней, і до того ж вони не стануть м'якими, так як буде виконуватися тільки одна вибірка і порівняння з двома можливими результатами (світло чи тінь).
    Метод Variance Shadow Mapping (VSM) дозволяє отримати м'які тіні через одну вибірку з shadow map. Логіка shadow mapping змінена, і замість простого порівняння глибин, використовується усереднене значення глибин навколо текселя і усереднене значення квадрата глибини. Ці параметри дозволяють використати нерівність Чебишева, яка в залежності від різниці поточної глибини фрагмента і усередненої глибини, а також від середньоквадратичного відхилення, дозволяє імітувати плавний перехід від світла до тіні. Далі показано кроки необхідні для імплементації Variance Shadow Mapping.
    Перший прохід рендерінгу VSM сходий до першого проходу в стандартному Shadow Mapping. Розраховується нормалізована відстань від фрагмента до джерела світла в межах [0, 1]. Додатково в ціль рендерінгу зберігається квадрат відстані від фрагмента до світла. Тобто потрібна двоканальна ціль рендерінгу. В наступному прикладі показано шейдер, який зберігає необхідні значення:
    float worldSpaceDistance = distance(u_lightPosition, o_worldSpacePosition.xyz);
    float dist = (worldSpaceDistance - u_nearFarPlane.x) /
                  (u_nearFarPlane.y - u_nearFarPlane.x) + u_depthOffset;

    resultingColor.r = clamp(dist, 0, 1);
    resultingColor.g = resultingColor.r * resultingColor.r;
    Далі, необхідно розмити shadow map, яка містить лінійну глибину і квадрат лінійної глибини. Це можна зробити через двопрохідний фільтр розмиття Гауса з ядром розміром 5x5 текселів. В результаті отримаємо усереднену глибину і усереднений квадрат глибини. Також для shadow map можна увімкнути білінійну чи анізотропну фільтрацію.
    Текстура з усередненими глибинами передається в останій прохід рендерінгу VSM. Вершинний шейдер аналогічний до вершинного шейдера в другому проході Shadow Mapping. В фрагментному шейдері стандартним способом визначаються текстурні координати в shadow map, які відповідають поточному фрагмента.
    Далі, розраховується середньоквадратичне відхилення глибини і застосовується нерівність Чебишева. Спочатку проводиться вибірка з shadow map. Нехай середнє значення глибини буде M1, а середнє значення квадрату глибини буде M2. Тоді середньоквадратичне відхилення σ буде рівне:
    Квадрат середньоквадратичного відхилення
    З нерівності Чебишева випливає наступна система рівнянь для розрахунку інтенсивності світла:
    Розподілення глибини в районі точки
    Інтенсивність освітлення виходить максимальною, коли різниця між поточною глибиною і локальною усередненою глибиноє є мінімальною. Зважуючим значенням є середньоквадратичне відхилення. Якщо середньоквадратичне відхилення велике, то воно збільшує інтенсивність освітлення у місцях де глибина сильно відрізняється від середньої глибини.
    Але Variance Shadow Maps має недоліки. Серед них - розтікання освітлення (Light bleeding). Light bleeding проявляється як освітлення у місцях, які повинні бути у тіні. Найчастіше ефект виникає у місцях де є високочастотні зміни світла і тіні. Цей артефакт можна зменшити через встановлення обмеження для результату обрахованого через нерівність Чебишева. Також можна автоматично затінювати полігони, які напрямлені від світла.
    Variance Shadow Maps
    Наступний фрагментний шейдер показує імплементацію Variance Shadow Maps:
    #version 330

    in vec4 o_shadowCoord;
    in vec3 o_normal;
    in vec3 o_worldPos;

    layout(location = 0) uniform sampler2D u_textureShadowMap;
    uniform vec3 u_lightPosition;
    uniform float u_minVariance;
    uniform float u_lightBleedingLimit;
    uniform vec2 u_nearFarPlane;

    layout(location = 0) out vec4 resultingColor;

    ////////////////////////////////////////////////////////

    float reduceLightBleeding(float p_max, float amount)
    {
        return clamp((p_max-amount)/ (1.0-amount), 0.0, 1.0);
    }

    void main(void)
    {
        /////////////////////////////////////////////////////////

        // current distance to light
        vec3 fromLightToFragment = u_lightPosition - o_worldPos.xyz;
        float worldSpaceDistance = length(fromLightToFragment);
        float currentDistanceToLight = clamp((worldSpaceDistance - u_nearFarPlane.x)
            / (u_nearFarPlane.y - u_nearFarPlane.x), 0, 1);

        /////////////////////////////////////////////////////////

        // get blured and blured squared distance to light
        vec3 projectedCoords = o_shadowCoord.xyz / o_shadowCoord.w;
        vec2 depths = texture(u_textureShadowMap, projectedCoords.xy).xy;

        float M1 = depths.x;
        float M2 = depths.y;
        float M12 = M1 * M1;

        float p = 0.0;
        float lightIntensity = 1.0;
        if(currentDistanceToLight >= M1)
        {
            // standard deviation
            float sigma2 = M2 - M12;

            // when standard deviation is smaller than epsilon
            if(sigma2 < u_minVariance)
            {
                sigma2 = u_minVariance;
            }

            // chebyshev inequality - upper bound on the
            // probability that fragment is occluded
            float intensity = sigma2 / (sigma2 + pow(currentDistanceToLight - M1, 2));

            // reduce light bleeding
            lightIntensity = reduceLightBleeding(intensity, u_lightBleedingLimit);
        }

        /////////////////////////////////////////////////////////

        resultingColor.rgb = vec3(lightIntensity);
        resultingColor.a = 1.0;
    }
    Exponent shadow mapping (ESM)
    Variance Shadow Mapping використовує два канали shadow map, а отже в два рази більше пам'яті ніж стандартна версія Shadow Mapping. Також можуть бути сильно помітними недоліки викликані light bleeding. Замість Variance Shadow Mapping можна використати Exponential Shadow Maps (EMS). Exponential Shadow Maps використовує тільки один канал shadow map, а також зменшує ефект від light bleeding.
    Метод Exponential Shadow Maps базується на тому, що різниця між глибиною з shadow map і глибиною поточного фрагмента повинна бути більша або рівна нулю. Коли фрагмент освітлений, то різниця (в ідеальному випадку!) повинна бути 0, а якщо фрагмент в тіні, то більше нуля.
    Факт на якому базується Exponential Shadow Maps
    Освітленість можна апроксимувати через функцію залежну від степеня експоненти. Значення такої функції буде дуже швидко спадати до нуля при від'ємній різниці глибин. Коли глибини будуть однаковими, то exp(z-d) = exp(0) = 1
    Апроксимація теста глибини
    Для того, щоб можна було регулювати наскільки швидким буде перехід від світла до тіні використовують константу c. Чим більше c, тим різкіше буде переходити світло у тінь. Для 32 бітних shadow map, можна використати значення с=80, для 16 бітних - c=30 і менше.
    Апроксимація теста глибини
    Як видно на графіку, функція повертає 1.0 для фрагментів в яких немає різниці між глибиною фрагмента і розмитою глибиною з shadow map. Коли різниця від'ємна, то повертається велике значення, яке більше за одиницю. В такому випадку необхідно обрати 1 як рівень освітленості фрагмента. При великій різниці d-z, функція повертає значення близькі до 0, тобто фрагмент у тіні.
    Функція швидко зростає при зменшені різниці між d і z
    При збережені глибин в shadow map, замість збереження лінійної глибини в межах [0, 1], зберігається exp(cd):
    resDepth = exp(c * depth);
    Як і для VSM, при використані ESM можна розмити карту глибин фільтром розмиття Гауса. Чим більше розмиття, тим м'якіші тіні. Занадто велике розмиття призведе до винекнення артефактів.
    В проході рендерінгу, що розраховує тіні, освітлення кожного фрагмента розраховується як exp(cz) * exp(-cd). Далі наведено приклад імплементації Exponent Shadow Mapping:
    #version 330

    in vec4 o_shadowCoord;
    in vec3 o_normal;
    in vec3 o_worldPos;

    layout(location = 0) uniform sampler2D u_textureShadowMap;
    uniform vec3 u_lightPosition;
    uniform float u_depthMultiplier;
    uniform vec2 u_nearFarPlane;
    uniform float u_epsilon;


    layout(location = 0) out vec4 resultingColor;

    /////////////////////////////////////////////////

    void main(void)
    {
        /////////////////////////////////////////////////

        // current distance to light
        vec3 fromLightToFragment = u_lightPosition - o_worldPos.xyz;
        float worldSpaceDistance = length(fromLightToFragment);
        float currentDistanceToLight = clamp((worldSpaceDistance - u_nearFarPlane.x)
                / (u_nearFarPlane.y - u_nearFarPlane.x), 0, 1);

        /////////////////////////////////////////////////

        // get blured exp of depth
        vec3 projectedCoords = o_shadowCoord.xyz / o_shadowCoord.w;
        float depthCExpBlured = texture(u_textureShadowMap, projectedCoords.xy).r;

        // current exp of depth
        float depthCExpActual = exp(- (u_depthMultiplier * currentDistanceToLight));
        float expFactor = depthCExpBlured * depthCExpActual;

        // Threshold classification for high frequency artifacts
        if(expFactor > 1.0 + u_epsilon)
        {
            expFactor = 1.0;
        }

        /////////////////////////////////////////////////

        resultingColor.rgb = vec3(expFactor);
        resultingColor.a = 1.0;
    }
    При високочастотних деталях в сцені, ESM може неправильно інтерпретувати данні. В такому разі замість однієї вибірки можна провести декілька вибірок, як в PCF. Фрагменти для яких необхідна додаткова обробка можна знайти через перевірку - чи обрахована інтенсивність освітлення не більша ніж 1.0 + epsilon.
    Exponential Shadow Maps також має свої недоліки. Перший з них є тим, що EMS це трюк, який дозволяє добре і швидко відобразити м'які тіні, але немає під собою фізичного підгрунтя. Через це може виникати light bleeding, але менше ніж для VSM. Також тіні біля об'єктів можуть бути занадто світлими біля до об'єктів і темнішими на далекій відстані, а повинно бути навпаки. Але при правильних параметрах EMS та VSM є якісними і швидкими методами розрахунку тіней.
    Variance Shadow Maps



    Sun and Black Cat- Ігор Дихта (igor dykhta email) © 2007-2014