Як зробити LOD для тисяч анімованих персонажів у Unity

Посібник із LOD для натовпу в Unity з vertex animation textures: чому важливі вершини, а не трикутники, LOD як нова сітка і вибір рівня для кожної сутності.

Три версії одного персонажа-скелета зі зменшенням кількості трикутників зліва направо

LOD для тисяч анімованих персонажів у Unity будуйте за кількістю вершин, а не трикутників: у vertex animation textures анімація рахується для кожної вершини. Нижчі рівні підготуйте вручну, вершини найнижчого рівня приєднайте до поверхні оригіналу, а рівень вибирайте для кожного персонажа окремо. У нашій tower defense грі для ПК це при 3 000 ворогів підняло FPS у наближенні до натовпу з 62 до 120.

Це стосується ігор, де тисячі персонажів рухаються на екрані одночасно: tower defense, стратегій і survival. Наша гра зроблена в Unity з ECS і є в референціях. LOD означає level of detail: віддаленого персонажа малюють із простішої сітки. На зображеннях справжні сітки нашого ворога-скелета, а не кадри з гри.

Чому згенерований LOD не допомагає натовпу

Unity вміє генерувати рівні LOD під час імпорту моделі (функція Mesh LOD). Для тисяч персонажів це має дві проблеми.

Перша: вигляд. Автоматичне спрощення рвало тонкі кінцівки й ламало UV, тобто прив'язку текстури до сітки.

Друга: ціна анімації. Звичайна скелетна анімація надто дорога для тисяч сутностей, тому замість неї використовують vertex animation textures (VAT). VAT є текстурою, де кожен стовпець належить одній вершині, а кожен рядок одному кадру анімації. Шейдер читає положення вершини з текстури, тож ціна анімації залежить від кількості прочитаних вершин, зокрема й під час малювання тіней.

Верхню межу показує тест із примусовим згенерованим рівнем на всіх персонажах (Unity генерує також рівні 3 і 4, build, 1080p, 3 000 ворогів, тіні ввімкнено):

Примусовий рівень Трикутники FPS
LOD 0 11 мільйонів 72
LOD 1 6 мільйонів 96
LOD 2 3,68 мільйона 120
LOD 4 1,43 мільйона 201

Запас для прискорення великий. Його треба використати без помітних дірок і стрибків між рівнями.

Рахуйте вершини, а не трикутники

Удвічі менше трикутників саме по собі майже не допомагає. LOD із половиною трикутників і 996 із початкових 1 521 вершин заощадив 1,9 мс на кадр, LOD із 217 вершинами 6,4 мс. На кожному рівні стежте за обома числами.

Вершин часто буває забагато. Вершина подвоюється на кожному шві UV, бо одне положення тоді потребує двох текстурних координат. Наші моделі з AI-генератора мали UV, порізані на сотні острівців: скелет мав 1 521 вершину, але лише 608 різних поєднань положення й нормалі.

Крок 1: Зменште кількість вершин у LOD 0

Перша економія не коштує жодної деталі. Об'єднайте вершини з однаковим положенням і схожою нормаллю (різниця до 25°), розгорніть UV заново зі швами лише на гострих ребрах (понад 70°) і перерахуйте текстуру зі старої по трикутниках. Скелет зменшився з 1 521 до 1 090 вершин, інші типи ворогів на 19-29 %, за тієї ж форми.

Потім записуйте у VAT лише стовпці вершин, які справді використовує якийсь рівень. Це заощадило 40-60 % пам'яті анімацій.

Крок 2: LOD 1 зі спрощення зі спільною текстурою

LOD 1 є LOD 0, спрощеним приблизно до 52 % трикутників. Спрощуйте частинами (кожне поєднання кістки та зв'язної частини сітки) і блокуйте вершини на межах частин. Голові дайте більший бюджет, бо силует голови око впізнає в натовпі.

Важливо залишити текстуру LOD 0. Рівень із власною текстурою або іншою формою видно під час переходу навіть за схожого силуету.

Нижче приблизно 50 % трикутників спрощення з наявних вершин рве тонкі частини й окремі шматки. Далі потрібен інший підхід.

Три версії персонажа-скелета поруч зі зменшенням кількості трикутників
Той самий скелет на трьох рівнях: 873, 448 і 330 трикутників, 1 090, 705 і 520 вершин.

Крок 3: LOD 2 як нова сітка, приєднана до оригіналу

Найнижчий рівень є новою замкненою сіткою. Потовщіть оригінал шаром, пройдіть voxel remesh і спростіть до цільової кількості (у нас 200-460 трикутників залежно від типу ворога). Результат обтягніть на поверхню оригіналу, UV зробіть автоматично. Текстуру запечіть з оригіналу в тій самій щільності, що й LOD 0, щоб збігалися й mipmap.

Нова сітка не має ваг анімації. Тому кожну її вершину приєднайте до найближчого трикутника оригіналу. Збережіть три стовпці цього трикутника й барицентричні ваги (частки, які задають положення точки в трикутнику).

Схема: червона вершина спрощеної сітки з'єднана пунктиром із трьома вершинами трикутника оригіналу
Вершина нижчого рівня (червона) рухається за трьома вершинами найближчого трикутника оригіналу.

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

для кожного кадру f:
    VAT[приєднана, f] = w0 * VAT[a, f] + w1 * VAT[b, f] + w2 * VAT[c, f]

Крок 4: Перевіряйте дірки автоматично

Зображення з програми для моделювання надійно дірок не покажуть. Достатньо простої перевірки: відрендеріть оригінал і кандидата з 11 напрямків із роздільністю 120 px і порівняйте силуети.

Діркою вважайте пікселі оригіналу всередині контуру кандидата, які кандидат не покриває, і пікселі кандидата більш ніж на 2 px поза контуром оригіналу. У нас межа 1 % дірок і 4-6 % втрат. Перевірка має враховувати відсікання задніх граней залежно від матеріалу. Межі ми встановили за тим, де переходи в грі було видно оком.

Крок 5: Вибирайте рівень для кожного персонажа окремо

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

Уся мапа одночасноНатовп точок одного кольору, усі персонажі мають однаковий рівень
Для кожної сутностіНатовп точок смугами за відстанню від камери, близькі червоні, далекі сірі
Вигляд згори, камера внизу. Колір позначає рівень LOD: червоний 0, помаранчевий 1, сірий 2.

Застереження щодо версії: документація Unity 6.2 зазначає, що Entities Graphics завжди використовує LOD 0. Вибір для кожної сутності за формулою нижче ми застосовуємо в Unity 6.6, тому заздалегідь перевірте поведінку своєї версії:

LOD = log2(d * C / D) * нахил + зсув
C = поріг * 2 * tan(fov / 2) / висота_кадру

Тут d означає відстань від камери, а D діаметр світових меж сутності. Нахил і зсув підберіть так, щоб рівень 1 починався на 93,3, а рівень 2 на 167,9 метра (камера з fov 30° і 1440 px). Глобальний поріг LOD встановіть на висоту кадру / 1440, тоді відстані не залежать від роздільності.

Дві помилки, яких варто уникати:

  • Масштаб. Діаметр D рахуйте зі світових меж, тобто з масштабом prefab. Сітки для VAT часто мають малі одиниці, а prefab збільшує їх у 2-4 рази. Без цього гра весь час малює LOD 0 і нічого не повідомляє. Перевірте це за кількістю трикутників у звичайному режимі порівняно з примусовим LOD. Поворот сутності збільшує осьові межі, тому в нас діаметр множиться на коефіцієнт 1,22.
  • Запікання. Крива вибору зберігається під час запікання SubScene, і запікання не помічає зміни в сітці. Після її редагування примусово перезапечіть SubScene.

Результати й обмеження

Build для macOS, 2560 × 1440, 3 000 ворогів, тіні ввімкнено, гра зупинена після поразки (анімації VAT далі працюють, тож вимірювання дійсне для графіки):

Сцена До Після
Уся мапа 90 FPS 130 FPS
Наближення до натовпу 62 FPS 120 FPS

На Steam Deck (1280 × 800, 8 обчислювальних блоків RDNA 2) це 75-80 FPS на всій мапі. Найближчий кадр у натовпі має 33 FPS із високою якістю тіней та 64 без тіней. Тіні, отже, найбільша стаття витрат: LOD знижує і їхню ціну, але вимкнення все одно дає великий стрибок.

Чого LOD не вирішує: перевірка дірок працює в T-позі, тож не помітить розтягнення в екстремальних позах. Один тип ворогів досі перевищує межу дірок у LOD 2. На обчислення руху орди LOD не впливає, про це стаття про навігацію орди в Unity DOTS, про реакції на вибухи fake ragdoll.

Потрібна допомога з продуктивністю гри в Unity або симуляцією натовпу? Напишіть нам.

← Більше в категорії Tower Defense