Прокачування pointlight тіней у Unity

image

Аналогів подібних тіней для точкового джерела світла (Pointlight з ефектом розмиття на відстані, що імітує arealight) в комп'ютерних іграх я чомусь досі не зустрічав. Скрізь — або повністю запечені тіні, або «лампочки» взагалі без тіней, максимум — звичайна PCF-фільтрація. Хоча для спрямованого сонячного світла вже давно застосовуються PCSS-тіні (GTA5, наприклад). В Unreal є цікавий алгоритм схожий рейтрейсингу, який малює гарні arealight-тіні, але тільки для статичної геометрії (потрібна генерація додаткових обсягів). У Unity ж все зовсім погано — м'яко фільтрується тільки сонячне світло, а «прожектори» і «лампочки» в прольоті.

Якщо на «прожектори» у Unity висить хоч якась худо-бідно білінійна фільтрація, то на точкових «лампочки» ось такий жах:



Невже, додати звичайну PCF-фільтрування — це такий удар по продуктивності? Навіть якщо і удар, то чому не зробити можливість включення цієї фільтрації опціонально?

Як з'ясувалося, удару по продуктивності немає. У Unity 5 діє технологія differed lighting (відкладене освітлення), і чим більше в кадрі джерел світла з динамічною тінню, тим гірше продуктивність. А вже яка фільтрація у цих тіней — не настільки важливо. Здійснюється одне і те ж число проходів, обробляється стільки ж пікселів, цей момент якраз і потребує оптимізації сцени. А число вибірок з кубічної текстури глибини «лампочки» нехай і впливає на продуктивність, але дуже незначно (в районі пари FPS).

Також варто сказати, що розмивати отриману тінь image-ефектом не вийде. Немає ніякої прошарку між вибіркою з глибини і текстури накладенням світла, така вже система в Unity. А шкода, можна було б у чомусь виграти.

Отже, міняємо алгоритм

Створювати спеціальні матеріали заради м'яких тіней копітко, вносити зміни в наявні шейдери скриптом (як це зроблено в ShadowSoftener) — теж якось не дуже зручно. Набагато швидше і практичніше застосувати тіні відразу до всіх шейдерам в проекті (вбудованим, рукописним, скачаним), змінивши файл «UnityShadowLibrary.cginc», який знаходиться в директорії редактора: "...\Unity5\Editor\Data\CGIncludes".

Знаходимо цей шматочок, який відповідає за тінь від точкового джерела світла:

#if defined (SHADOWS_SOFT)

float z = 1.0/128.0;
float4 shadowVals;
shadowVals.x = SampleCubeDistance (vec+float3( z, z, z));
shadowVals.y = SampleCubeDistance (vec+float3(-z, z, z));
shadowVals.z = SampleCubeDistance (vec+float3(-z, z, z));
shadowVals.w = SampleCubeDistance (vec+float3( z-z-z));
half4 shadows = (shadowVals < mydist.xxxx) ? _LightShadowData.rrrr : 1.0 f;

return dot(shadows,0.25); 

#else 


На місце можна поставити будь-який вподобаний нам алгоритм, але спочатку розберемо вхідні параметри:

vec — чотиривимірний вектор. x,y і z — це напрям від «лампочки» до пікселя. Роблячи вибірку за цим значенням з допомогою SampleCubeDistance, ми отримуємо відстань затенившего об'єкта. Якщо відстань менше довжини vec, тінь на піксель не падає. (mydist — те саме відстань, але це не вхідний праметр, він обчислюється вище в цьому ж файлі) У кожній вибірці координата зміщується на фіксовану величину, створюючи ефект неоднорідною кордону. Не смійтеся. Професійні програмісти, розробляють Unity, називають це «м'якої тінню».

_LightShadowData.r — це значення повзунка, налаштовує яскравість тіні. Можна скористатися ним, наприклад, для зміни ступеня розмиття тіні або для зміни якогось іншого параметра, щоб налагоджувати шейдер безпосередньо в редакторі. На жаль, я так і не з'ясував, на що впливають інші компоненти _LightShadowData. Мабуть, для точкового джерела світла більше параметрів немає.

Повертаємо з функції яскравість (0 до 1.0), тобто множник, який у всіх шейдери діє під ім'ям atten (attenuation), тому змінена тінь буде діяти теж у всіх шейдери, що підтримують затінення.

Щоб побачити зміни, знайдіть папку свого проекту, в ній – папку «Library». Видаляємо з цієї папки директорію «ShaderCache» і перезапускаємо Unity. Я теж перезапускал Unity після кожного редагування шейдера, це нервувало і ускладнювало налагодження.

От і все.

Я спробував ось такий варіант, замазавши краю непоказним дизерингом:

#if defined (SHADOWS_SOFT)

// Чим менше, тим тінь размытестее
float downscale = f 32.0;

// Випадковий вектор
const float3 rndseed = float3(12.9898,78.233,45.5432);
float3 randomvec = float3( dot(vec,rndseed) , dot(vec.yzx,rndseed) , dot(vec.zxy,rndseed) );
randomvec = frac(sin(randomvec) * 43758.5453);

// Ось ці вектора для зміщень
float3 xvec = normalize(cross(vec,randomvec));
float3 yvec = normalize(cross(vec,xvec));
float3 vec1 = xvec / downscale;
float3 vec2 = yvec / downscale;

float4 shadowVals;

// Вибірки з кубмапы
shadowVals.x = SampleCubeDistance (vec+vec1);
shadowVals.y = SampleCubeDistance (vec+vec2);
shadowVals.z = SampleCubeDistance (vec-vec1);
shadowVals.w = SampleCubeDistance (vec-vec2);

// Змішуємо
half4 shadows = (shadowVals < mydist.xxxx) ? _LightShadowData.rrrr : 1.0 f;
return dot(shadows,0.25);

#else 


Вибірок стільки ж, скільки і в страшній канонічної реалізації, але, принаймні, пікселі не муляють очі:



Не довго думаючи (але довго налагоджуючи) зробив ось таку реалізацію PCF4x4 з «ручний» білінійної фільтрації (шейдери Unity можна, звичайно, вставляти команди DirectX11, серед яких є і білінійна фільтрація тіней кубмапа, але я пішов по шляху універсальності і зробив додатковий ряд вибірок для «ручного» згладжування, не вийшло 16 вибірок, а 25).

image

Або навіть ось так:



Це не чесний PCSS, а більш швидка і проста реалізація. Максимальна розмиття — це те ж саме PCF4x4. А чітка тінь поблизу затенителя досягається збільшенням різкості. Між іншим, ефект shapen теж багато де застосовується. Тут же у нас симбіоз, за допомогою якого можна добитися гарного ефекту.

Вище згаданий бігунок налаштовує кут переходу м'якою тіні в різку. Цей параметр можна підбирати різними для різних «лампочок», виходячи з розмірів приміщень, в якому вони знаходяться.

Код останньої реалізації я помістив під кат. Він трохи громіздкий і неохайний, адже я не використовував двовимірний цикл вибірок, а прописував їх вручну, щоб знизити кількість команд і кількість використовуваних змінних, а заодно застосувати операції з векторами там, де можна обчислити відразу кілька змінних. Плюс я принципово не використовував умовні переходи, замінивши їх математикою. Тому дивіться на свій страх і ризик.

Некрономикон
inline half UnitySampleShadowmap (float3 vec)
{
float mydist = length(vec) * _LightPositionRange.w;
mydist *= 0.97; // bias

const float downscale = 128.0 f;
const float sat_mult = 312.0 f;

#if defined (SHADOWS_SOFT)

#define shadow_close_scalefactor (_LightShadowData.r + 0.001 f)

// Віртуальні осі і їх перетворення в справжні
#define xvec main_axis.zxy
#define yvec main_axis.yzx
#define VIRTUAL_COORD(x,y) (ceilvec + xvec*x + yvec*y)

// Нам треба дізнатися, уздовж якої площини направити початковий вектор
// (вісь вперед для правильного напряму пікселів)
half3 main_axis = abs(vec)*1666.666 f;
main_axis = normalize(clamp(main_axis.xyz - main_axis.yzx,0.0 f,1.0 f)*clamp(main_axis.xyz - main_axis.zxy,0.0 f,1.0 f));

// Впираємось вектор в кубмап
vec /= abs(dot(vec,main_axis));

// Це центр віртуального пікселя, щодо якого буде вестися інтерполяція кольору
fixed3 ceilvec = ceil(vec*downscale) / downscale;

// Від нуля до одиниці - значення для інтерполяції між 4-ма положеннями
fixed4 lerp_delta;
vec = (ceilvec - vec) * downscale;
lerp_delta.x = dot(vec * xvec,1.0f);
lerp_delta.y = dot(vec * yvec,1.0 f);
lerp_delta.z = 1.0 f - lerp_delta.x;
lerp_delta.w = 1.0 f - lerp_delta.y;

// Підготовка до вибіркам
main_axis /= downscale;
ceilvec -= (xvec + yvec)*0.5 f;

//Змінні і патерни для вибірки
float4 shadowVals, distance_sums, distancesides; fixed4 shadowsides, shadow_sums, distancesides_nums, distance_nums;
#define DISTANCE_COMPARE_X4(sum,distance_sum,distance_num) shadowVals = mydist.xxxx - shadowVals; sum = dot(clamp(shadowVals *sat_mult,0.0 f,1.0 f),1.0 f); distance_sum = dot(clamp(shadowVals, 0.0 f,100.0 f),1.0 f); distance_num = dot(clamp(shadowVals *sat_mult,0.0 f,1.0 f),1.0 f)
#define DISTANCE_COMPARE_X3(sum,distance_sum,distance_num) shadowVals = mydist.xxxx - shadowVals; sum = dot(clamp(shadowVals.xyz*sat_mult,0.0 f,1.0 f),1.0 f); distance_sum = dot(clamp(shadowVals.xyz,0.0 f,100.0 f),1.0 f); distance_num = dot(clamp(shadowVals.xyz*sat_mult,0.0 f,1.0 f),1.0 f)
#define DISTANCE_COMPARE_X1(sum,distance_sum,distance_num) shadowVals.x = mydist - shadowVals.x; sum = clamp(shadowVals.x *sat_mult,0.0 f,1.0 f); distance_sum = clamp(shadowVals.x, 0.0 f,100.0 f); distance_num = clamp(shadowVals.x *sat_mult,0.0 f,1.0 f)

// Вибірка значень з центральної області - для всіх положень однакова
// Перші 4 пікселя
shadowVals.x = SampleCubeDistance (VIRTUAL_COORD(-1.0 f,-1.0 f));
shadowVals.y = SampleCubeDistance (VIRTUAL_COORD(0.0 f,-1.0 f));
shadowVals.z = SampleCubeDistance (VIRTUAL_COORD(1.0 f,-1.0 f));
shadowVals.w = SampleCubeDistance (VIRTUAL_COORD(1.0 f,0.0 f));
DISTANCE_COMPARE_X4(shadowsides.x,distancesides.x,distancesides_nums.x);
// Другі 4 пікселя
shadowVals.x = SampleCubeDistance (VIRTUAL_COORD(-1.0 f,-0.0 f));
shadowVals.y = SampleCubeDistance (VIRTUAL_COORD(-1.0 f,1.0 f));
shadowVals.z = SampleCubeDistance (VIRTUAL_COORD(0.0 f,1.0 f));
shadowVals.w = SampleCubeDistance (VIRTUAL_COORD(1.0 f,1.0 f));
DISTANCE_COMPARE_X4(shadowsides.y,distancesides.y,distancesides_nums.y);
// Центральний піксель
shadowVals.x = SampleCubeDistance (VIRTUAL_COORD(0.0 f,0.0 f));
DISTANCE_COMPARE_X1(shadowsides.z,distancesides.z,distancesides_nums.z);
// Розкладаємо суми за 
shadow_sums = dot(shadowsides.xyz,1.0 f).xxxx;
distance_sums = dot(distancesides.xyz,1.0 f).xxxx;
distance_nums = dot(distancesides_nums.xyz,1.0 f).xxxx + fixed4(0.01 f,0.01 f,0.01 f,0.01 f);

// Вибірка значень для індивідуальних областей

// Ліво
shadowVals.x = SampleCubeDistance (VIRTUAL_COORD(-2.0 f,-1.0 f));
shadowVals.y = SampleCubeDistance (VIRTUAL_COORD(-2.0 f,0.0 f));
shadowVals.z = SampleCubeDistance (VIRTUAL_COORD(-2.0 f,1.0 f));
DISTANCE_COMPARE_X3(shadowsides.x,distancesides.x,distancesides_nums.x);
// Низ
shadowVals.x = SampleCubeDistance (VIRTUAL_COORD(-1.0 f,-2.0 f));
shadowVals.y = SampleCubeDistance (VIRTUAL_COORD(0.0 f,-2.0 f));
shadowVals.z = SampleCubeDistance (VIRTUAL_COORD(1.0 f,-2.0 f));
DISTANCE_COMPARE_X3(shadowsides.y,distancesides.y,distancesides_nums.y);
// Право
shadowVals.x = SampleCubeDistance (VIRTUAL_COORD(2.0 f,-1.0 f));
shadowVals.y = SampleCubeDistance (VIRTUAL_COORD(2.0 f,0.0 f));
shadowVals.z = SampleCubeDistance (VIRTUAL_COORD(2.0 f,1.0 f));
DISTANCE_COMPARE_X3(shadowsides.z,distancesides.z,distancesides_nums.z);
// Верх
shadowVals.x = SampleCubeDistance (VIRTUAL_COORD(-1.0 f,2.0 f));
shadowVals.y = SampleCubeDistance (VIRTUAL_COORD(0.0 f,2.0 f));
shadowVals.z = SampleCubeDistance (VIRTUAL_COORD(1.0 f,2.0 f));
DISTANCE_COMPARE_X3(shadowsides.w,distancesides.w,distancesides_nums.w);
// Розкладаємо суми по відповідним секціям
shadow_sums += (shadowsides.xzxz + shadowsides.yyww);
distance_sums += (distancesides.xzxz + distancesides.yyww);
distance_nums += distancesides_nums.xzxz + distancesides_nums.yyww;

// Кутові точки
shadowVals.x = SampleCubeDistance (VIRTUAL_COORD(-2.0 f,-2.0 f));
shadowVals.y = SampleCubeDistance (VIRTUAL_COORD(2.0 f,-2.0 f));
shadowVals.z = SampleCubeDistance (VIRTUAL_COORD(-2.0 f,2.0 f));
shadowVals.w = SampleCubeDistance (VIRTUAL_COORD(2.0 f,2.0 f));
// Вважаємо вручну, бо нефіг дефайны плодити заради одного виклику
shadowVals = mydist.xxxx - shadowVals;
shadow_sums += clamp(shadowVals*sat_mult,0.0 f,1.0 f);
distance_sums += clamp(shadowVals,0.0 f,1.0 f);
distance_nums += clamp(shadowVals*sat_mult,0.0 f,1.0 f);

// Интерполируем між чотирма позиціями
shadow_sums.x = dot(shadow_sums * lerp_delta.xzxz * lerp_delta.yyww, 1.0 f) / 16.0 f;
distance_sums.x = dot(clamp((distance_sums/distance_nums),0.0 f,1.0 f) * lerp_delta.xzxz * lerp_delta.yyww, 1.0 f);

// Збільшуємо констраст поблизу джерела тіні
fixed contrastfactor = 1.0 f - clamp(distance_sums.x/shadow_close_scalefactor,0.0 f,1.0 f);
shadow_sums.x = clamp((shadow_sums.x - 0.5 f) * (1.0 f + contrastfactor*4.0 f)+0.5 f,0.0 f,1.0 f);

return 1.0 f - shadow_sums.x;

#else

// Тінь з дизерингом і 4 вибірками для спрощеної деталізації

vec = normalize(vec) * 0.5 f; // Компенсація розміру тіні

const float3 rndseed = float3(12.9898,78.233,45.5432);
float3 randomvec = float3( dot(vec,rndseed) , dot(vec.yzx,rndseed) , dot(vec.zxy,rndseed) );
randomvec = frac(sin(randomvec) * 43758.5453);

float3 vec1 = normalize(cross(vec,randomvec)) / downscale;
float3 vec2 = normalize(cross(vec,vec1)) / downscale;

float4 shadowVals;

shadowVals.x = SampleCubeDistance (vec+vec1);
shadowVals.y = SampleCubeDistance (vec+vec2);
shadowVals.z = SampleCubeDistance (vec-vec1);
shadowVals.w = SampleCubeDistance (vec-vec2);

shadowVals = mydist.xxxx - shadowVals;
fixed4 shadows = clamp(shadowVals*sat_mult,0.0 f,1.0 f);

return dot(shadows,0.25);

#endif

}



Якщо вас не влаштовує конкретно цей підхід, реалізуйте в рамках Unity більш відповідні алгоритми. Буду тільки радий такій активності, адже мене правда пригнічує застій у Unity у плані реалтайм тіней, який тягнеться вже багато років.

P. S.: Найбільше мені хотілося б почути думки тих, хто працює безпосередньо з Unity і знає, що там можна, а що не можна, хоча швидше за все будуть розлогі роздуми про алгоритми, впилить які движок не вдасться через його закритої архітектури. Але і до розлогих роздумів з радістю приєднаюся і поясню деякі моменти.
Джерело: Хабрахабр
  • avatar
  • 0

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.