Counter Strike 2 Дим в Unreal Engine 5

Вступ

Ловіть безкоштовний проєкт! Наприкінці статті ви знайдете посилання на безкоштовний проєктний файл, щоб власноруч розібратися, як працює цей крутий ефект. 

А тепер, уявіть собі: ви кидаєте димову гранату у Counter-Strike 2, і замість звичних плоских частинок перед вами з’являється реальна, об’ємна хмара диму. Вона огинає перешкоди, реагує на світло та навіть змінюється від куль і вибухів!

CS2 перевертає уявлення про димові ефекти, використовуючи сучасні графічні технології. Тут задіяні Flood Fill для заповнення простору, Raymarching для рендерингу об’ємного диму та Render Target, що дозволяє кулям і гранатам взаємодіяти з ним у реальному часі.

Хочете дізнатися, як саме це працює? Давайте розбиратися!

Принцип роботи Raymarching у рендерингу диму

Що таке Raymarching

Raymarching (променевий прохід) – це метод обчислювальної графіки, при якому для кожного пікселя екрану виконується крокове просування (“маршрут”) вздовж променя з камери у сцену. На кожному кроці відбираються проби з об’ємних даних (напр. густина диму) і поступово накопичується колір та прозорість, поки промінь не вийде за межі об’єму або не досягне повного загасання світла. Іншими словами, на відміну від класичного ray tracing, де промінь зупиняється при попаданні в поверхню, raymarching “проштовхується” крізь весь об’єм і інтегрує вклад кожної малої частинки середовища.

Raymarching Smoke

Такий підхід ідеально підходить для рендерингу диму, хмар чи туману – тобто ефектів, де немає чіткої поверхні, а прозорість і колір змінюються всередині об’єму.

Чому Raymarching підходить для диму

Дим являє собою неоднорідне середовище, що поглинає та розсіює світло. Променевий прохід дозволяє чисельно інтегрувати вплив цього середовища на світло: скільки світла поглинається або розсіюється на шляху до глядача. Це дає реалістичний результат – м’які краї хмари, плавний перехід від прозорих до густих ділянок, ефекти об’ємного світла тощо. Метод особливо ефективний, коли потрібно обчислити, яка частина світла блокується неоднорідним об’ємом диму, маючи лише дані про густину диму в декількох точках.

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

Більше інформації по реалізації волюметричного диму з використанням Raymarching ви зможете знайти на YouTube-каналі Enrique Ventura:

 

Алгоритм Flood Fill у створенні диму

Як працює Flood Fill для диму

Алгоритм починається з осередку, де вибухнула димова граната (джерело диму). Цій початковій комірці надається максимальна щільність диму і вона додається до черги обробки. Далі алгоритм ітеративно бере комірки з черги і “заливає” (заповнює) їх незаповнених сусідів димом, якщо:

– сусідня комірка знаходиться в межах допустимого об’єму (наприклад, не за стіною),

– загальна кількість заповнених димом комірок ще не перевищує поріг

Кожну нову заповнену комірку диму додаємо до черги. Алгоритм триває, поки дим “не скінчиться” (тобто заповнено потрібну кількість вокселів) або поки всі можливі комірки в радіусі не будуть заповнені. Результатом є набір координат вокселів, що містять дим.

Формування об’єму диму

Щоб визначити форму диму, використовується алгоритм Flood Fill (заливка простору). Дим поширюється по воксельній сітці, заповнюючи доступні області. Заповнення зупиняється, коли вичерпується об’єм або дим досягає перешкоди, таких як стіни чи великі об’єкти.

В Unreal Engine алгоритм реалізований рекурсивним обходом сітки вокселів. Ось код, який використовується для заповнення простору димом:

void ASmokeGrenade::FloodFillSphere(const FVector& Start, int x, int y, int z, float ThresholdRadius)
{
    floodCount += 1; // Лічильник викликів функції, щоб контролювати кількість оброблених комірок

    // Перевірка виходу за межі сітки
    if (x < 0 || x >= xMax || y < 0 || y >= yMax || z < 0 || z >= zMax)
        return;

    // Отримання індексу вокселя у сітці
    int tempIndex = GetIndex(x, y, z, yMax, zMax);

    // Якщо ця комірка вже була заповнена, виходимо
    if (Grid[tempIndex] == true)
        return;

    // Отримання світової позиції поточного вокселя
    FVector CurrentPosition = ConvertGridLocationToWorldLocation(Start, x, y, z);

    // Обчислення відстані від центру диму до поточного вокселя (без витратних операцій кореня)
    float Distance = (CurrentPosition - GetActorLocation()).FVector::SquaredLength();

    // Перевірка, чи поточна комірка виходить за межі допустимого радіуса диму
    if (Distance > (pow(ThresholdRadius, 2)))
        return;

    // Перевірка на перешкоди (наприклад, стіни або великі об’єкти) у цій точці
    bool bIsBlocked = GetWorld()->OverlapAnyTestByChannel(
        CurrentPosition, 
        FQuat::Identity, 
        ECollisionChannel::ECC_WorldStatic, 
        FCollisionShape::MakeSphere(50 * VoxelSize)
    );

    // Якщо є перешкода, не заповнюємо цю комірку димом
    if (bIsBlocked)
        return;

    // Позначаємо цю комірку як заповнену
    Grid[tempIndex] = true;

    // Додаємо її у список заповнених позицій
    GridPosition.Add(CurrentPosition);

    // Рекурсивно поширюємо дим у шістьох напрямках (всі сусідні вокселі)
    FloodFillSphere(Start, x + 1, y, z, ThresholdRadius);
    FloodFillSphere(Start, x - 1, y, z, ThresholdRadius);
    FloodFillSphere(Start, x, y + 1, z, ThresholdRadius);
    FloodFillSphere(Start, x, y - 1, z, ThresholdRadius);
    FloodFillSphere(Start, x, y, z + 1, ThresholdRadius);
    FloodFillSphere(Start, x, y, z - 1, ThresholdRadius);
}
Як працює ця реалізація
  • Функція отримує координати вокселя та перевіряє, чи виходить він за межі сітки.
  • Перевіряється, чи ця комірка вже заповнена – якщо так, функція завершується.
  • Обчислюється відстань від центру гранати до поточного вокселя. Якщо вона перевищує заданий ThresholdRadius, то ця комірка не заповнюється.
  • Виконується перевірка на перешкоди. Якщо у поточній комірці є об’єкт, який блокує дим, то ця область не заповнюється.
  • Комірка позначається як заповнена і додається до списку заповнених позицій.
  • Функція рекурсивно викликає сама себе для всіх шести сусідніх комірок, заповнюючи простір навколо.

Таким чином, дим поширюється у всіх напрямках, але не проходить крізь стіни та обмежується радіусом ThresholdRadius.

Використання Render Target для взаємодії з димом

Інтерактивність диму

Щоб реалізувати взаємодію диму з пострілами та вибухами, використовується Render Target – текстура, в яку записуються дані про вплив на дим у реальному часі. Ця система дозволяє динамічно змінювати об’єм диму на основі взаємодії з середовищем, і забезпечує такі ефекти, як пробиття куль, розсіювання вибухами та поступове відновлення диму.

Запис інформації в Render Target

Основна реалізація ґрунтується на перетворенні позиції попадання в глобальні UV-координати, які використовуються в матеріалі для запису змін у Render Target. Кожен постріл або вибух записує дані про свою взаємодію з димом, задаючи:

  • Позицію UV – координати, куди вноситься зміна.
  • Радіус впливу – визначає, наскільки велика зона буде змінена.
  • Густину впливу – використовується для контролю ефекту (зменшення або відновлення диму).
Реєстрація влучань куль


При проходженні кулі через дим система визначає всі точки попадання у вокселі та записує їх у Render Target. Для кожного влучання:

  • Обчислюється позиція у світових координатах.
  • Перетворюється в глобальні UV-координати.
  • У матеріалі, що працює з Render Target, записується ця точка з малим радіусом, що створює ефект локального пробиття диму.
    Оскільки куля пролітає через весь об’єм диму, вона записує відразу кілька точок у Render Target вздовж своєї траєкторії, що створює візуально коректний ефект розриву хмари.
Реєстрація вибухів гранат

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

  • Визначається центр вибуху.
  • Він перетворюється в глобальні UV-координати.
  • В Render Target записується всього одна точка, але з великим радіусом, що створює ефект потужного витіснення диму.
    На відміну від куль, що записують багато дрібних точок, гранати впливають на велику область одночасно, розганяючи дим у місці вибуху.
Матеріал для запису змін у Render Target

Матеріал, який використовується для роботи з Render Target, приймає три основні параметри:

  • UV-координати впливу.
  • Радіус зміни.
  • Густину впливу.
    Це дозволяє отримувати різні ефекти в залежності від типу взаємодії. Наприклад:
  • Куля записує багато дрібних точок вздовж траєкторії.
  • Граната записує одну велику область.

Unreal Engine Render Target Material

Відновлення диму

Щоб дим не залишався зміненим назавжди, після певного часу відбувається його відновлення. Це реалізується через другий Render Target, який поступово додає густину диму в області, де він був зменшений. Процес виглядає так:

  • У Render Target для відновлення записується поступове збільшення густини.
  • Матеріал диму читає обидві текстури (основну і відновлюючу) та відновлює дим у втрачених зонах.
    В результаті дірки від куль зникають через короткий час, а дим після вибуху повертається повільніше.

Реалізація стилізованої версії диму

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

Відмінності стилізованого диму від реалістичного

Основний принцип залишається тим самим: використовується той самий шейдер, що і для реалістичного диму, але з кількома ключовими змінами:

    • Закруглена прозорість
      У стилізованому варіанті ми позбуваємось плавного градієнта прозорості. Всі значення прозорості закруглені до 1, що означає, що дим тепер виглядає як чіткі форми, без плавного розсіювання.
    • Різкі колірні переходи
      Для передачі кольору ми використовуємо колірну карту (Color Ramp), але без інтерполяції між кольорами. Це означає, що колір диму переходить різко з одного відтінку в інший, без плавних градієнтів.

  • Використання патернів у текстурах
    Щоб додати стилізованості, в шейдер додано підтримку патернів та текстурних масок. Це дозволяє створювати унікальні стилізовані варіації диму, які можуть виглядати, наприклад, у стилі коміксів або мультфільмів.

Візуальний стиль у дусі коміксів


Такий підхід дозволяє легко змінювати візуальний стиль диму, наприклад, зробити його схожим на ефекти з гри Hi-Fi Rush або інші коміксові стилі. Завдяки різким переходам кольорів і можливості додавати унікальні патерни, дим набуває яскраво вираженого художнього вигляду.

Висновок і проєктні файли

Тепер настав час застосувати все на практиці! Ми підготували для вас безкоштовний проєктний файл, який ви можете завантажити за цим посиланням:

🔗 [ЗАВАНТАЖИТИ ПРОЄКТНИЙ ФАЙЛ]

Тепер ви самі можете дослідити, як виконано цей ефект, розібратися у всіх нюансах і навіть створити власну варіацію!

Волюметричний дим у Counter-Strike 2 – це яскравий приклад того, як сучасні графічні технології не лише підвищують візуальну якість, а й додають нові геймплейні можливості. Поєднання Raymarching для рендерингу, Flood Fill для формування об’єму та Render Target для інтерактивності дозволяє створити динамічний та реалістичний дим, що реагує на оточення. Такі технології відкривають нові горизонти для розробників, дозволяючи їм легко адаптувати стиль ефекту під будь-яку гру.

Якщо вам подобається світ візуальних ефектів, хочете навчитися створювати такі ж вражаючі ефекти і працювати у геймдеві, запрошуємо вас у More VFX Academy!  На наших курсах ви навчитеся створювати ефекти для ігор з нуля та працювати з найсучаснішими технологіями у цій сфері.

Приєднуйтесь до нас і станьте VFX-легендою! 🚀

🔗 [Дізнатися більше про курс]

 

Залишити коментар

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *

Прокрутка до верху