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

Shadow Mapping для точкових джерел світла в OpenGL та GLSL

У цьому уроці показано, як використовувати метод Shadow Mapping для точкових джерел світла в OpenGL та GLSL. Метод Shadow Mapping для точкових джерел світла відрізняється від методу Shadow Mapping для напрямлених джерел світла чи для джерел світла типу прожектор. Для напрямлених джерел світла і для джерел світла типу прожектор вистачає однієї текстури shadow map, щоб зберегти інформацію про відстань від джерела світла до найближчої точки поверхні. Але точкове джерело світла не має напряму і світить в усі сторони навколо себе. Тож для того, щоб розрахувати усі тіні від точкового джерела світла, потрібно зберегти інформацію в shadow map для усього простору навколо точкового джерела світла. Методи покращення результатів shadow mapping розглянуті в попередньому уроці.
Однієї двовимірної текстури не вистачить для збереження інформації про відстані з усіх сторін простору навколо точкового джерела світла. Потрібно використати кубічну текстуру в ролі shadow map. Для цього для кожної грані кубічної текстури зберігається (рендериться) відстань від точкового джерела світла до найближчих об'єктів у сцені. Для того, щоб не рендерити сцену шість разів, можна використати одночасний рендерінг в декілька буферів. У найновіших версіях OpenGL можливо за один прохід провести рендерінг в усі грані кубічної текстури (через геометричний шейдер). Також, якщо камера дивиться на сцену, наприклад, зверху, то можна знехтувати рендерінгом в верхню грань кубічної текстури.
Розглянемо наступне зображення. Зеленим показано об'єкти навколо точкового джерела світла. Чорним показано чотири різні ділянки простору, які записуються у різні грані кубічної текстури (інші дві ділянки простору не показані, так як це вигляд зверху). Блакитними позначками показано точки, відстань до яких збережеться в кубічній shadow map, і які будуть освітлені при погляді на них. Поверхня за колами буде в тіні.
Shadow Map для точкового джерела світла повинна бути прорахована у всіх напрямках
Далі показано як у шейдері зберегти лінійну відстань від джерела світла до точки. Запис відстані буде здійснюватися в текстуру, яка під'єднана як ціль для запису кольору (так як у буфері глибини зберігається нелінійна відстань). До лінійної відстані додається зсув, щоб у другому проході Shadow Mapping не виникало z-fighting. На зображенні показано інвертовану лінійну відстань від джерела світла до кожного фрагмента.
Інвертована лінійна відстань від джерела світла до кожного фрагмента
Параметри кожної грані кубічної текстури для shadow map (як GL_COLOR_ATTACHMENT):
glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, shadowMapSize, shadowMapSize,
            0, GL_RED, GL_FLOAT, nullptr);
Вершинний шейдер для збереження лінійної відстані від точкового джерела світла до поверхні, найближчої з точки зору джерела світла:
#version 330

// атрибути
layout(location = 0) in vec3 i_position; // xyz - position

// матриці
uniform mat4 u_modelMat;
uniform mat4 u_viewMat;
uniform mat4 u_projMat;

// світові координати вершини до фрагментного шейдера
out vec4 o_worldSpacePosition;

void main(void)
{
   // світові координати
   o_worldSpacePosition = u_modelMat * vec4(i_position, 1);

   // вершина в екранний простір
   gl_Position = u_projMat * u_viewMat * o_worldSpacePosition;
}
Фрагментний шейдер для збереження лінійної відстані від точкового джерела світла до поверхні, найближчої з точки зору джерела світла:
#version 330

// світові координати фрагмента
in vec4 o_worldSpacePosition;

// відстань у фреймбуфер
layout (location = 0) out float resultingColor;

// позиція точкового джерела світла
uniform vec3 u_lightPos;
// відстань від світла до близької та дальньої площини відтинання
uniform vec2 u_nearFarPlane;
// додатковий зсув від світла
uniform float u_depthOffset;

void main(void)
{
   // відстань до світла
   float distanceToLight = distance(u_lightPos, o_worldSpacePosition.xyz);
   // нормалізуємо відстань, щоб проміжок між площинами відтинання
   // потрапляв у межі [0, 1]

   resultingColor = (distanceToLight - u_nearFarPlane.x) /
            (u_nearFarPlane.y - u_nearFarPlane.x) + u_depthOffset;
   // усі інші відстані вміщуються у межі [0, 1]
   resultingColor = clamp(resultingColor, 0, 1);
}
Використання кубічної текстури як shadow map призводить до того, що повинен використовуватися інший метод визначення текстурних координат ніж в стандартному Shadow Mapping. Замість використання матриці тіні для розрахунку текстурних координат і відстані до світла, знову вручну розраховується лінійна відстань від світла до кожної точки. Для доступу в кубічну shadow map використовується напрям від світла до точки. Лінійна відстань з shadow map порівнюється з лінійною відстанню поточного фрагмента до джерела світла. Якщо відстань з текстури shadow map більша, то фрагмент освітлений, інакше - в тіні.
Shadow Mapping для точкового джерела світла з однією вибіркою
Вершинний шейдер для другого проходу Shadow Mapping для точкових джерел світла:
#version 330

// атрибути
layout(location = 0) in vec3 i_position; // xyz - position
layout(location = 1) in vec3 i_normal; // xyz - normal

// матриці
uniform mat4 u_modelMat;
uniform mat4 u_viewMat;
uniform mat4 u_projMat;

// позиція вершини в світових координатах у фрагментний шейдер
out vec4 o_worldPosition;

void main(void)
{
   // позиція вершини в світових координатах
   o_worldPosition = u_modelMat * vec4(i_position, 1);

   // екранна позиція світла
   gl_Position = u_projMat * u_viewMat * o_worldPosition;
}
Фрагментний шейдер для другого проходу Shadow Mapping для точкових джерел світла:
#version 330

// shadow map
layout(location = 0) uniform samplerCube u_shadowCubeMap;

// позвиція фрагмента в світових координатах
in vec4 o_worldPosition;

// колір у фреймбуфер
layout(location = 0) out vec4 resultingColor;

// позиція точкового джерела світла
uniform vec3 u_lightPos;
// відстані до ближньої і дальньої площин відсікання
uniform vec2 u_nearFarPlane;

void main(void)
{
   // різниця між позицією світла і позицією фрагмента
   vec3 fromLightToFragment = u_lightPos - o_worldPosition.xyz;
   // розрахунок нормалізованої відстані до світла
   float distanceToLight = length(fromLightToFragment);
   float currentDistanceToLight = (distanceToLight - u_nearFarPlane.x) /
            (u_nearFarPlane.y - u_nearFarPlane.x);
   currentDistanceToLight = clamp(currentDistanceToLight, 0, 1);
   // нормалізований напрям від світла до поточного фрагмента
   fromLightToFragment = normalize(fromLightToFragment);

   // вибірка з shadow map
   float referenceDistanceToLight = texture(u_shadowCubeMap, -fromLightToFragment).r;
   // порівняння глибин для розрахунку фактора затінення
   float shadowFactor = float(referenceDistanceToLight > currentDistanceToLight);

   // вивід кольору
   resultingColor.rgb = vec3(shadowFactor);
   resultingColor.a = 1;
}
Для кубічних текстур може використовуватися тип семплера samplerCubeShadow, який автоматично порівнює відстань з текстури shadow map з відстанню поточного фрагмента до світла, і повертає фактор затіненості. Перші три компоненти другого аргумента задають напрям для вибірки, а четвертий - відстань для порівняння.
layout(location = 0) uniform samplerCubeShadow u_shadowCubeMap;
float shadowFactor = texture(u_shadowCubeMap, vec4(-fromLightToFragment,
        currentDistanceToLight));
Аналогічно до двовимірних текстур, кубічну текстуру можна налаштувати так, що буде порівняно чотири сусідні текселі з кубічної текстури, і повернуто зважене значення фактора затінення.
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_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
Алгоритм Percentage Closer Filtering (PCF) для кубічної shadow map потребує ще більшої кількості вибірок для досягнення хорошої якості тіней, ніж при використанні двовимірної shadow map. Причина цього в тому, що вибірка з shadow map проходить через напрям, і немає простого метода для того, щоб визначити напрям зсуву в кубічній текстурі вздовж грані, а не вперед чи назад.
Тож для розмиття країв тіней може використовуватися масив зсувів, що охоплюють усі сторони тривимірного простору. При цьому фактор затінення для більшої половини напрямків буде однаковим, так як напрями будуть мало відрізнятися (особливо для зсусів орієнтованих вздовж і проти напряму вибірки). Проводити вибірку з кубічної текстури можна ненормалізованими векторами, тож не потрібно нормалізовувати вектор вибірки кожного разу при додаванні до нього PCF зсуву.
Shadow Mapping для точкового джерела світла розрахований з допомогою PCF. Також в сцені враховується невелика кількість дифузного освітлення
На зображенні показано вектор вибірки та масив векторів зсувів. Вектори зсувів позначені червоним кольором (перпендикулярні вектору вибірки) дозволяють утворити хороші вектори вибірки, значення фактору затінення яких буде розраховане з різних текселів shadow map. Вектори зсувів, які позначені оранжовим кольором, роблять меншу зміну напряму вектора вибірки, ніж червоні. Фактори затінення цих векторів слабо розмивають тіні, так як можуть рахуватися по однаковим текселям shadow map. Також нижні і відповідні верхні оранжеві вектори зсувів будуть давати майже однакові результати. Сині вектори зсувів мають ту ж орієнтацію, що і початковий вектор вибірки, і не змінюють його напряму, і як наслідок відповідають одному і тому самому фактору затенення.
Вектор вибірки та масив векторів зсувів
// масив зсувів для вектора вибірки
vec3 gridSamplingDisk[20] = vec3[]
(
   vec3(1, 1, 1), vec3(1, -1, 1), vec3(-1, -1, 1), vec3(-1, 1, 1),
   vec3(1, 1, -1), vec3(1, -1, -1), vec3(-1, -1, -1), vec3(-1, 1, -1),
   vec3(1, 1, 0), vec3(1, -1, 0), vec3(-1, -1, 0), vec3(-1, 1, 0),
   vec3(1, 0, 1), vec3(-1, 0, 1), vec3(1, 0, -1), vec3(-1, 0, -1),
   vec3(0, 1, 1), vec3(0, -1, 1), vec3(0, -1, -1), vec3(0, 1, -1)
);

// функція для порівняння значення з shadow map з поточною відстанню
void sampleShadowMap(in vec3 baseDirection, in vec3 baseOffset,
      in float curDistance, inout float shadowFactor, inout float numSamples)
{
   shadowFactor += texture(u_shadowCubeMap,
      vec4(baseDirection + baseOffset, curDistance));
   numSamples += 1;
}

void main(void)
{
   // ...

   float shadowFactor = 0;
   float numSamples = 0;

   // розрахунок відхилення залежно від відстані до світла
   float diskRadius = (1.0 + (1.0 - currentDistanceToLight) * 3) / sizeOfCubeTex;

   // розрахунок фактора затіненості для усіх зсувів
   for(int i=0; i<20; i++)
   {
      sampleShadowMap(-fromLightToFragment, gridSamplingDisk[i] * diskRadius,
         currentDistanceToLight, shadowFactor, numSamples);
   }

   // усереднення фактора затінення
   shadowFactor /= numSamples;

   // вивід кольору з фреймбуфер
   resultingColor.rgb = vec3(shadowFactor);
   resultingColor.a = 1;
}
Для точного розрахунку напрямів вибірок для Percentage Closer Filtering в кубічній shadow map, можна використати метод описаний в уроці про розмиття кубічних текстур. Можна створити масив напрямів в просторі дотичних розміром 3x3 чи 4x4 напрями. І перетворювати напрями з простору дотичних в простір напряму для вибірки з кубічної shadow map. Недоліком такого підходу є велика кількість множень напрямів на матрицю повороту в фрагментному шейдері.
Зсуви напрямку додатково можуть домножуватися на відхилення. Чим більше відхилення, тим більшим буде відстань між вибірками з shadow map. В ідеалі відхилення повинне бути таким, щоб кожна вибірка повертала значення з іншого текселя (чи групи інших текселів при лінійному фільтрі порівняння). При більшому радіусі відхилення може виникати цікавий ефект, який показано на зображенні. Щось схоже, до ефекту розхляпаних тіней.
Ефект розхляпаних тіней
Для покращення результатів Shadow Mapping для точкових джерел світла можна використати методи покращення результатів метода Shadow Mapping



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