sunLoadingImage
whowedImag
decoration left 1
decoration left 2
transhome
transprojects
transgallery
transarticles
decoration rigth
English
Українська
Show/Hide search bar
black cat logo variable logo
[21 Бер 2013]

Parallax Mapping в GLSL

У цьому уроці буде розглянуто створення ефекту паралаксу з допомогою GLSL та OpenGL (аналогічні принципи розрахунку паралаксу використовуються і в DirectX). Буде розглянуто такі методи побудови паралаксу, як Parallax Mapping, Parallax Mapping with offset limiting, Steep Parallax Mapping, Relief Parallax Mapping і Parallax Occlusion Mapping (POM). Також розглянуто розрахунок самозатінення для елементів паралаксу - м'які тіні (soft shadows). На наступних зображеннях наведено результати використання паралаксу в GLSL в порівнянні з простим затіненням та normal mapping.
Просте затінення по Бліну-Фонгу плоскої поверхні - немає жодних деталей
Затінення по Бліну-Фонгу при нормалях розрахованих через Normal Mapping. Розрахунок освітлення відбувається зі зміненими нормалями, але немає відчуття, що виступи у 3D
Parallax Mapping у поєднанні з Normal Mapping та стандартним розрахунком освітлення. Здається, що поверхня ввігнута, а пірамиди виступають. Але насправді це плоска поверхня.
М'які тіні для Parallax Mapping
Основи Parallax Mapping
В комп'ютерній графіці Parallax Mapping це вдосконалена версія Normal Mapping, яка не тільки змінює освітлення, але і створює ілюзію реальної випуклості плоского об'єкта в 3D просторі. При цьому не генерується додаткової геометрії і початкова геометрія залишається без змін. На попередніх зображеннях показано порівняння parallax mapping та normal mapping. Може здатися, що у випадку parallax mapping відбувся зсув геомертії, але насправді змінені тільки текстурні координати, які використовуються для доступу в текстури кольору та нормалей об'єкта.
Для розрахунку Parallax Mapping необхідна текстура, кожен піксель якої відповідає висоті на поверхні. Також значення з текстури висот можна інтерпретувати, як розмір заглиблення у поверхню. В такому разі необхідно інвертувати карту висот. Parallax mapping розглянутий у цьому уроці буде викоритовувати карту висот для визначення глибини заглиблень. Чорний колір (0) у карті висот буде означати відсутність заглиблення, а білий (1) - заглиблення максимальної глибини.
Також у наступних прикладах використорвується текстура з кольорами об'єкта та текстура з нормалями об'єкта. Зазвичай карта нормалей генерується з карти висот. Якщо карта висот використовується для визначення глибини, то перед генерацією карти нормалей, потрібно інвертувати текстуру висот. Також карта нормалей та карта висот повинні бути об'єднаними в одну текстуру (висота в альфа канал), але для зручності у цьому уроці використано три окремі текстури. Приклад текстур для Parallax Mapping показаний на наступних зображеннях.
Карта висот. В наступних прикладах карта висот використовується для визначення глибини, а не висоти випуклостей. Білі частани текстури означають найбільші впадини.
Текстура з дифузним кольором обєкта
Текстура з нормалями для Normal Mapping

Розрахунок ефекту Parallax Mapping грунтується на визначенні текстурних координат таким чином, щоб поверхня здавалася об'ємною. Ефект розраховується в фрагментному шейдері для кожного видимого фрагмента. Розглянемо наступне зображення. Рівень 0 відповідає нульовому вглибленню в поверхню, тобто це є поверхня об'єкта, яка використовується у Normal Mapping. Рівень зі значенням 1 відповідає максимальному заглибленню. Реальна геометрія об'єкта залишається незмінною, і завжди лежить на рівні 0. Крива показує значення, що знаходяться в карті глибини і як ці значення повинні інтерпретуватися.
Фрагмент, який обробляється, показано жовтим квадратом. Фрагменту від вершиного шейдера передалися текстурні координати T0. Вектор від камери до фрагмента рівний V. Проведемо вибірку з текстури глибини по текстурним координатам T0. Дістанемо значення - H(T0) = 0.55. Значення не рівне 0.0, а отже фрагмент в реальності не лежить на поверхні. Під ним є заглиблення. Необхідно продовжити вектор V до найближчого пересічення з поверхнею заданою через карту глибини. Текстурні координати Т1 відповідають місцю такого пересічення в точці H(T1). По Т1 проводиться вибірка кольору та нормалей об'єкта, і розраховується освітлення.
Усі методи Parallax Mapping зводяться до того, щоб якнайточніше знайти текстурні координати T1, у місці де вектор погляду перетинається з поверхнею заданою картою висот/глибин.
Суть Parallax Mapping
Базовий шейдер для Parallax Mapping
Розрахунки Parallax Mapping, як і Normal Mapping, проводяться у просторі дотичних (tangent space). Тож вектори до світла (L) і до камери (V) повинні бути переведені в простір дотичних. Після розрахунку нових текстурних координат з врахуванням паралаксу відбувається розрахунок замозатінення та освітлення по методу NormalMapping. Розрахунок Parallax Mapping розміщений у функції parallaxMapping(), розрахунок самозатінення у parallaxSoftShadowMultiplier(), а розрахунок Normal Mapping поміщений у normalMappingLighting(). Далі наведено базові вершинний та фрагментний шейдер, які будуть використовуватися для усіх далі розглянутих методів Parallax Mapping:
// Базовий вершинний шейдер
#version 330

// attributes
layout(location = 0) in vec3 i_position; // xyz - position
layout(location = 1) in vec3 i_normal; // xyz - normal
layout(location = 2) in vec2 i_texcoord0; // xy - texture coords
layout(location = 3) in vec4 i_tangent; // xyz - tangent, w - handedness

// uniforms
uniform mat4 u_model_mat;
uniform mat4 u_view_mat;
uniform mat4 u_proj_mat;
uniform mat3 u_normal_mat;
uniform vec3 u_light_position;
uniform vec3 u_camera_position;

// дані для фрагментного шейдера
out vec2 o_texcoords;
out vec3 o_toLightInTangentSpace;
out vec3 o_toCameraInTangentSpace;

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

void main(void)
{
   // трансформувати в світові координати
   vec4 worldPosition = u_model_mat * vec4(i_position, 1);
   vec3 worldNormal = normalize(u_normal_mat * i_normal);
   vec3 worldTangent = normalize(u_normal_mat * i_tangent.xyz);

   // розрахувати вектор до камери і вектор до світла
   vec3 worldDirectionToLight = normalize(u_light_position - worldPosition.xyz);
   vec3 worldDirectionToCamera = normalize(u_camera_position - worldPosition.xyz);

   // відновити bitangent з нормалі і tangent
   vec3 worldBitangnent = cross(worldNormal, worldTangent) * i_tangent.w;

   // перетворення напряму до світла в tangent space
   o_toLightInTangentSpace = vec3(
         dot(worldDirectionToLight, worldTangent),
         dot(worldDirectionToLight, worldBitangnent),
         dot(worldDirectionToLight, worldNormal)
      );

   // перетворення напряму до камери в tangent space
   o_toCameraInTangentSpace= vec3(
         dot(worldDirectionToCamera, worldTangent),
         dot(worldDirectionToCamera, worldBitangnent),
         dot(worldDirectionToCamera, worldNormal)
      );

   // передати текстурні координати у фрагментний шейдер
   o_texcoords = i_texcoord0;

   // розрахувати екранні координати вершини
   gl_Position = u_proj_mat * u_view_mat * worldPosition;
}

// Базовий фрагментний шейдер
#version 330

// дані від вершиного шейдера
in vec2 o_texcoords;
in vec3 o_toLightInTangentSpace;
in vec3 o_toCameraInTangentSpace;

// текстури
layout(location = 0) uniform sampler2D u_diffuseTexture;
layout(location = 1) uniform sampler2D u_heightTexture;
layout(location = 2) uniform sampler2D u_normalTexture;

// колір, який буде збережено у фреймбуфер
out vec4 resultingColor;

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

// Через це значення задається відносний розмір для Parallax Mapping
uniform float parallaxScale; // ~0.1

//////////////////////////////////////////////////////
// Розрахунок Parallax Mapping
// Повертає зсунуті текстурні координати, а також глибину, на якій закінчено пошук

vec2 parallaxMapping(in vec3 V, in vec2 T, out float parallaxHeight)
{
   // ...
}

//////////////////////////////////////////////////////
// Розраховує самозатінення для паралаксу - м'які тіні
// Повертає фактор затінення

float parallaxSoftShadowMultiplier(in vec3 L, in vec2 initialTexCoord,
                                       in float initialHeight)
{
   // ...
}

//////////////////////////////////////////////////////
// Розраховує освітлення по методу Normal Mapping
// Повертає колір освітленого фрагменту

vec4 normalMappingLighting(in vec2 T, in vec3 L, in vec3 V, float shadowMultiplier)
{
   // відновити нормаль для Normal Mapping
   vec3 N = normalize(texture(u_normalTexture, T).xyz * 2 - 1);
   vec3 D = texture(u_diffuseTexture, T).rgb;

   // заповнююче освітлення
   float iamb = 0.2;
   // дивузне освітлення
   float idiff = clamp(dot(N, L), 0, 1);
   // блікове освітлення
   float ispec = 0;
   if(dot(N, L) > 0.2)
   {
      vec3 R = reflect(-L, N);
      ispec = pow(dot(R, V), 32) / 1.5;
   }

   vec4 resColor;
   resColor.rgb = D * (ambientLighting + (idiff + ispec) * pow(shadowMultiplier, 4));
   resColor.a = 1;

   return resColor;
}

/////////////////////////////////////////////
// Початок виконання фрагментного шейдера
void main(void)
{
   // нормалізувати вектори після інтерполяції
   vec3 V = normalize(o_toCameraInTangentSpace);
   vec3 L = normalize(o_toLightInTangentSpace);

   // отримати нові текстурні кооординати врахувавши паралакс
   float parallaxHeight;
   vec2 T = parallaxMapping(V, o_texcoords, parallaxHeight);

   // отримати фактор затінення для паралаксу
   float shadowMultiplier = parallaxSoftShadowMultiplier(L, T, parallaxHeight - 0.05);

   // розрахувати освітлення для зсунутих текстурних координат
   resultingColor = normalMappingLighting(T, L, V, shadowMultiplier);
}
Parallax Mapping і Parallax Mapping with offset limiting
Найпростішою версією Parallax Mapping є апроксимація зсуву текстурних координат. Такий Parallax Mapping дає більш-менш правильні результати тільки, коли карта висот є гладкою і не містить багато дрібних деталей. Інакше, при великих кутах між вектором погляду (V) і нормаллю (N), буде неправильно розрахований ефект паралаксу. Суть методу апроксимації зсуву текстурних координат в наступному:
  • дістати з карти висот глибину H(T0), яка відповідає поточним текстурним координатам.
  • зсунути початкові текстурні координати вздовж вектора погляду (V), врахувавши глибину H(T0).
  • Зсув текстурних координат вздовж вектора погляду відбувається наступним чином. Так як вектор погляду V є перетвореним у tangent space, і tangent space будувався по текстурним координатам, то компоненти x i y вектора V можна використовувати без жодних трансформацій як напрям зсуву текстурних координат вздовж вектора погляду V. Компонента Z вектора V - нормальна компонента. Компонетни x і y можна поділити на z, або залишити такими як є. Якщо не ділити на z, то метод називається Parallax Mapping with offset limiting. Відсутність ділення на z дозволяє зменшити спотворення результатів, коли кут між нормаллю і вектором погляду є великим. Таким чином додавши компоненти x і y вектора V до текстурних координат, отримаємо зсув текстурних координат в необхідному напрямку - вздовж вектора погляду V.
    Також потрібно, щоб зсув текстурних координат враховував величину H(T0). Для цього V.xy домножується на величину H(T0).
    Розмір Parallax Mapping задається через значення scale. На нього також потрібно домножити зсув текстурних координат. Придатними до використання значеннями для scale є значення від нуля до ~0.5. При більшому scale результати Parallax Mapping можуть сильно спотворюватися. Також scale можна зробити від'ємними. Тоді замість впуклостей, будуть випуклості. В такому разі також необхідно інвертувати Z компоненту нормалі з Normal Map. Далі наведена формула для розрахунку зсунутих текстурних координат TP:
    Ефект розмитості при занадто великому зсуві
    Апроксимація Parallax Mapping через зсув текстурних координат
    На наступному малюнку показано як значення з текстури глибини H(T0) використовується для визначення розміру зсуву вздовж вектора погляду. В даному випадку значення зсуву розраховане неточно, так як метод Parallax Mapping лиш наближує зсув, але не знаходить точного значення.
    Parallax Mapping
    Перевагою метода є те, що використовується тільки одна вибірка з текстури глибини і це майже не впливає на швидкодію GLSL шейдера. Далі наведено код шейдера з реалізацією найпростішого паралаксу:
    vec2 parallaxMapping(in vec3 V, in vec2 T, out float parallaxHeight)
    {
       // дістати глибину для даного фрагмента
       float initialHeight = texture(u_heightTexture, o_texcoords).r;

       // зсув parallax mapping
       vec2 texCoordOffset = parallaxScale * V.xy / V.z * initialHeight;

       // зсув у випадку Parallax Mapping With Offset Limiting
       texCoordOffset = parallaxScale * V.xy * initialHeight;

       // повернути нові текстурні координати з врахуванням зсуву Parallax Mapping
       return o_texcoords - texCoordOffset;
    }
    Steep Parallax Mapping
    Steep Parallax Mapping, на відміну від Parallax Mapping, не просто робить зсув текстурних координат без перевірки його правильності і доречності, а дійсно перевіряє, чи результат є наближеним до правильного. Суть цього методу полягає в тому, щоб розбити глибину поверхні на певну кількість шарів. Починаючи з найвищого шару робити вибірки з текстури глибини, кожного разу зсуваючи текстурні координати вздовж вектора виду. Якщо точка під поверхнею (глибина на якій лежить шар більша, ніж глибина, яка вибрана з текстури), то викоритовується поточний зсув.
    Приклад Steep Parallax Mapping наведений на наступному зображенні. Глибина поділена на 8 шарів. Кожен шар має висоту 0.125. Зсув текстурних координат на кожній ітерації рівний V.xy/V.z*scale/numLayers. Перебір починається від шару, де розташований фрагмент (жовтий квадрат). Наприклад:
  • Глибина поточного шару рівна 0. Глибина H(T0) рівна ~0.75. Глибина є більшою ніж глибина шару, тож переходимо до наступної ітерації.
  • Зсуваємо текстурні координати. Переходимо на наступний шар з глибиною 0.125. Глибина H(T1) рівна ~0.625. Глибина є більшою ніж глибина шару, тож переходимо до наступної ітерації.
  • Зсуваємо текстурні координати. Переходимо на наступний шар з глибиною 0.25. Глибина H(T2) рівна ~0.4. Глибина є більшою ніж глибина шару, тож переходимо до наступної ітерації.
  • Зсуваємо текстурні координати. Переходимо на наступний шар з глибиною 0.375. Глибина H(T3) рівна ~0.2. Глибина є меншою ніж глибина шару, тобто поточка точка на векторі V лежить під поверхнею. Наближене значення Tp текстурних координат знайдено.
  • Steep Parallax Mapping
    Як видно з зображення, текстурні координати знайдені через Steep Parallax Mapping не зовсім відповідають текстурним координатам в місці перетину вектора V та поверхні. Але ці текстурні координати є ближчими до перетину вектора виду V і поверхні ніж проста апроксимація через Parallax Mapping.
    Основним недоліком метода Steep Parallax Mapping є те, що він дискретизує поверхню на задану кількість шарів, і при малій кількості шарів стає дуже помітним ефект сходинок. Ця проблема вирішується збільшенням кількості шарів або через використання методів Relief Parallax Mapping чи Parallax Occlusion Mapping (POM). Приклад східчатості наведений на наступному зображенні. Також кількість шарів можна визначати в залежності від кута між нормаллю N та вектором погляду V. Далі наведено приклад реалізації Steep Parallax Mapping
    Ефект сходинок при недостатній якості Steep Parallax Mapping
    vec2 parallaxMapping(in vec3 V, in vec2 T, out float parallaxHeight)
    {
       // визначити кількість шарів в залежності від V і N
       const float minLayers = 5;
       const float maxLayers = 15;
       float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0, 0, 1), V)));

       // висота кожного шара
       float layerHeight = 1.0 / numLayers;
       // глибина поточного шара
       float currentLayerHeight = 0;
       // зсув текстурних координат на кожній ітерації
       vec2 dtex = parallaxScale * V.xy / V.z / numLayers;

       // поточні текстурні координати
       vec2 currentTextureCoords = T;

       // дістати першу глибину з текстури глибини
       float heightFromTexture = texture(u_heightTexture, currentTextureCoords).r;

       // доки точка над поверхнею
       while(heightFromTexture > currentLayerHeight)
       {
          // перейти до наступного шару
          currentLayerHeight += layerHeight;
          // зсунути текстурні координати вздовж вектора виду
          currentTextureCoords -= dtex;
          // дістати нову глибину з карти глибин
          heightFromTexture = texture(u_heightTexture, currentTextureCoords).r;
       }

       // повернути результат
       parallaxHeight = currentLayerHeight;
       return currentTextureCoords;
    }
    Relief Parallax Mapping
    Метод Relief Parallax Mapping розширює Steep Parallax Mapping і дозволяє точніше знайти зсув текстурних координат. Спочатку проводяться такі ж самі розрахунки як і в Steep Parallax Mapping. Після розрахунку Steep Parallax Mapping відомо два шара, між якими лежить перетин вектора V і поверхні. Покращити результат можна з допомогою метода половинного ділення.
    На наступному зображенні наведено принцип дії метода:
  • Після Steep Parallax Mapping відомо текстурні координати T2 і T3 між якими є перетин V.
  • Ділимо зсув текстурних координат і висоту шару на два.
  • Зсуваємо текстурні координати T3 проти напряму вектора V (в сторону T2) на зсув. Зменшуємо глибину на висоту шару.
  • (*) Робимо вибірку з текстури глибини. Ділимо зсув текстурних координат і висоту шару на два.
  • Якщо глибина з текстури більша за поточну глибину, то збільшуємо поточну глибину на розмір висоти шару, і зсуваємо текстурні координати за напрямом вектора виду V на зсув.
  • Якщо глибина з текстури менша за поточну глибину, то зменшуємо поточну глибину на висоту шару і зсуваємо поточні текстурні координати проти напряму вектора виду на зсув.
  • Повторюємо пошук від кроку (*) необхідну кількість разів.
  • Текстурні координати на останньому кроці паралаксу і є результатом роботи Relief Parallax Mapping.
  • Relief Parallax Mapping
    vec2 parallaxMapping(in vec3 V, in vec2 T, out float parallaxHeight)
    {
       // вибрати оптимальну кількість шарів
       const float minLayers = 10;
       const float maxLayers = 15;
       float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0, 0, 1), V)));

       // висота кожного шару
       float layerHeight = 1.0 / numLayers;
       // поточна глибина
       float currentLayerHeight = 0;
       // поточний зсув текстурних координат з кожним шаром
       vec2 dtex = parallaxScale * V.xy / V.z / numLayers;

       // поточні текстурні координати
       vec2 currentTextureCoords = T;

       // глибина з карти глибин
       float heightFromTexture = texture(u_heightTexture, currentTextureCoords).r;

       // доки точка на промені є над поверхнею
       while(heightFromTexture > currentLayerHeight)
       {
          // перейти до наступного шару
          currentLayerHeight += layerHeight;
          // зсув текстурних координат
          currentTextureCoords -= dtex;
          // нова висота з карти висот
          heightFromTexture = texture(u_heightTexture, currentTextureCoords).r;
       }

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

       // зменшити зсув і висоту шару на половину
       vec2 deltaTexCoord = dtex / 2;
       float deltaHeight = layerHeight / 2;

       // повернутися на середину попереднього шару
       currentTextureCoords += deltaTexCoord;
       currentLayerHeight -= deltaHeight;

       // уточнення результату по relief parallax mapping
       const int numSearches = 5;
       for(int i=0; i<numSearches; i++)
       {
          deltaTexCoord /= 2;
          deltaHeight /= 2;

          heightFromTexture = texture(u_heightTexture, currentTextureCoords).r;

          if(heightFromTexture > currentLayerHeight) // above surface
          {
             currentTextureCoords -= deltaTexCoord;
             currentLayerHeight += deltaHeight;
          }
          else // below surface
          {
             currentTextureCoords += deltaTexCoord;
             currentLayerHeight -= deltaHeight;
          }
       }

       // зберегти результати
       parallaxHeight = currentLayerHeight;    return currentTextureCoords;
    }
    Parallax Occlusion Mapping (POM)
    Parallax Occlusion Mapping (POM) як і Relief Parallax Mapping використовується для уточнення резултатів Steep Parallax Mapping. Замість використання методу половинного ділення для уточнення результату (як в Relief Parallax Mapping), у Parallax Occlusion Mapping використовується проста лінійна інтерполяція. Вхідними параметрами для інтерполяції є глибина шару після пересічення, попереднє і наступне значення глибини з текстури. Як видно з зображення знайдене пересічення лежить на перетині вектора виду та вектора між двома висотами H(T3) та H(T2). Знайдене пересічення доволі близько від реального пересічення.
    Наприклад, з зображення:
  • nextHeight = HT3 - currentLayerHeight;
  • prevHeight = HT2 - (currentLayerHeight - layerHeight)
  • weight = nextHeight / (nextHeight - prevHeight)
  • TP = TT2 * weight + TT3 * (1.0 - weight)
  • Parallax Occlusion Mapping дозволяє отримати хороші результати при відносно малій кількості вибірок з текстури глибини. Але Parallax Occlusion Mapping може пропускати деталі чи видавати неправильні результати, при дрібних деталях та різких змінах значень в карті глибини.
    Parallax Occlusion Mapping
    vec2 parallaxMapping(in vec3 V, in vec2 T, out float parallaxHeight)
    {
       // вибрати оптимальну кількість шарів
       const float minLayers = 10;
       const float maxLayers = 15;
       float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0, 0, 1), V)));

       // висота кожного шару
       float layerHeight = 1.0 / numLayers;
       // поточна глибина
       float curLayerHeight = 0;
       // поточний зсув текстурних координат з кожним шаром
       vec2 dtex = parallaxScale * V.xy / V.z / numLayers;

       // поточні текстурні координати
       vec2 currentTextureCoords = T;

       // глибина з карти глибин
       float heightFromTexture = texture(u_heightTexture, currentTextureCoords).r;

       // доки точка на промені є над поверхнею
       while(heightFromTexture > curLayerHeight)
       {
          // перейти до наступного шару
          curLayerHeight += layerHeight;
          // зсув текстурних координат
          currentTextureCoords -= dtex;
          // нова висота з карти висот
          heightFromTexture = texture(u_heightTexture, currentTextureCoords).r;
       }

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

       // попередні текстурні координати
       vec2 prevTCoords = currentTextureCoords + texStep;

       // висоти для лінійної інтерполяції
       float nextH = heightFromTexture - curLayerHeight;
       float prevH = texture(u_heightTexture, prevTCoords).r
                               - curLayerHeight + layerHeight;

       // пропорції лінійної інтерполяції
       float weight = nextH / (nextH - prevH);

       // інтерполяція текстурних координат
       vec2 finalTexCoords = prevTCoords * weight + currentTextureCoords * (1.0 - weight);

       // інтерполяція глибини
       parallaxHeight = curLayerHeight + prevH * weight + nextH * (1.0 - weight);

       // повернути результат
       return finalTexCoords;
    }
    Parallax Mapping і самозатінення
    Визначити чи фрагмент є у тіні можна застосовуючи принцип аналогічний до Steep Parallax Mapping. При цьому пошук треба здійснювати не вглиб поверхні, а вверх. Також пошук потрібно здійснювати не вздовж вектора погляду (V), а вздовж вектора до світла (L). Вектор до світла L, як і вектор погляду (V), є в tangent space і може напряму використовуватися для задання напряму зсуву текстурних координат. Результатом розрахунку затінення є фактор затінення [0, 1]. Під час розрахунку освітлення, інтенсивність дифузного та блікового освітлення домножується на фактор затінення.
    Поверхня з Parallax Oclussion Mapping без затінення
    Поверхня з Parallax Oclussion Mapping з затіненням
    Для розрахунку тіней з різкими контурами (hard shadows) можна проводити зсуви вздовж вектора L до першого перетину з поверхнею. Наприклад, на наступному зображенні, якщо H(TL1) менше висоти шару Ha, тобто точка є під поверхнею, то фрагмент є в тіні і фактор затінення рівний 0. Якщо немає перетину з поверхнею до виходу з рівня 0, то фрагмент освітлений і фактор затінення рівний 1. Якість тіней дуже залежить від кількості шарів, від модифікатора кроку scale і від кута між нормаллю (N) та вектором до світла (L). В багатьох випадках, при неправильних налаштуванях, тінь може не відповідати зсунутим елементам паралаксу.
    М'які тіні (Soft Shadows) розраховуються з врахуванням багатьох вибірок вздовж вектора L. До уваги беруться тільки точки, які є під поверхнею. Фактор обраховується, як різниця між поточною глибиною шару, і глибиною з текстури. Також повинна враховуватися відстань до фрагменту. Тому фактор домножується на (1 - stepIndex/numSteps). Для освітлення вибирається максимальне значення фактора затінення. Формула для розрахунку фактора затінення для м'яких тіней:
    Partial shadowing factor
    Final shadow factor
    Наприклад, для наступного зображення розрахунок фактора затінення відбувається наступним чином:
  • встановлюємо фактор затінення в 0, а кількість кроків 4.
  • крок вздовж L до Ha. Ha менше H(TL1), а отже є під поверхнею. Фактор розраховується як різниця між Ha - H(TL1). Це перший крок, а загальна кількість шарів рівна 4. Тому враховуючи відстань від фрагмента, фактор необхідно домножити на (1.0 - 1.0/4.0). Обираємо найбільший фактор між цим і найбільшим з попередніх.
  • крок вздовж L до Hb. Hb менше H(TL2), а отже є під поверхнею. Фактор розраховується як різниця між Hb - H(TL2). Це другий крок, а загальна кількість шарів рівна 4. Тому враховуючи відстань від фрагмента, фактор необхідно домножити на (1.0 - 2.0/4.0). Обираємо найбільший фактор між цим і найбільшим з попередніх.
  • усі наступні кроки дають результат над поверхнею.
  • фактор затінення розрахований.
  • Parallax self shadowing (Soft Shadows)
    float parallaxSoftShadowMultiplier(in vec3 L, in vec2 initialTexCoord,
                                           in float initialHeight)
    {
       float shadowMultiplier = 1;

       const float minLayers = 15;
       const float maxLayers = 30;

       // розраховувати затінення тільки для поверхонь орієнтованих до світла
       if(dot(vec3(0, 0, 1), L) > 0)
       {
          // розрахувати початкові параметри
          float numSamplesUnderSurface = 0;
          shadowMultiplier = 0;
          float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0, 0, 1), L)));
          float layerHeight = initialHeight / numLayers;
          vec2 texStep = parallaxScale * L.xy / L.z / numLayers;

          // поточні параметри
          float currentLayerHeight = initialHeight - layerHeight;
          vec2 currentTextureCoords = initialTexCoord + texStep;
          float heightFromTexture = texture(u_heightTexture, currentTextureCoords).r;
          int stepIndex = 1;

          while(currentLayerHeight > 0)
          {
             // якщо точка під поверхнею
             if(heightFromTexture < currentLayerHeight)
             {
                // розрахунок фактора затінення
                numSamplesUnderSurface += 1;
                float newShadowMultiplier = (currentLayerHeight - heightFromTexture) *
                                                 (1.0 - stepIndex / numLayers);
                shadowMultiplier = max(shadowMultiplier, newShadowMultiplier);
             }

             // зсув до наступного шару
             stepIndex += 1;
             currentLayerHeight -= layerHeight;
             currentTextureCoords += texStep;
             heightFromTexture = texture(u_heightTexture, currentTextureCoords).r;
          }

          if(numSamplesUnderSurface < 1)
          {
             shadowMultiplier = 1;
          }
          else
          {
             shadowMultiplier = 1.0 - shadowMultiplier;
          }
       }
       return shadowMultiplier;
    }



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