Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
Анімація тисяч риб за допомогою MultiMeshInstance3D
У цьому підручнику розглядається техніка, яка використовується в грі ABZU для відтворення й анімації тисяч риб за допомогою вершинної анімації та статики екземпляр сітки.
У Godot це можна зробити за допомогою спеціального Shader і MultiMeshInstance3D. Використовуючи наведену нижче техніку, ви можете рендерити тисячі анімованих об’єктів навіть на недорогому обладнанні.
Ми почнемо з анімації однієї риби. Потім ми побачимо, як поширити цю анімацію на тисячі риб.
Анімація однієї рибки
Почнемо з однієї риби. Завантажте свою модель риби в MeshInstance3D і додайте новий ShaderMaterial.
Ось риба, яку ми будемо використовувати для прикладів зображень, ви можете використовувати будь-яку модель риби, яка вам подобається.
Примітка
Модель риби в цьому підручнику створено QuaterniusDev і використовується за ліцензією Creative Commons. CC0 1.0 Універсальний (CC0 1.0) Призначення публічного домену https://creativecommons.org/publicdomain/zero/1.0/
Як правило, ви використовуєте кістки та Skeleton3D для анімації об’єктів. Однак кістки анімуються на процесорі, тому вам доведеться обчислювати тисячі операцій у кожному кадрі, і стає неможливим мати тисячі об’єктів. Використовуючи вершинну анімацію у вершинному шейдері, ви уникаєте використання кісток і натомість можете розрахувати повну анімацію в кількох рядках коду та повністю на GPU.
Анімація складатиметься з чотирьох ключових рухів:
Рух з боку в бік
Поворотний рух навколо центру риби
Хвилеподібний рух
Поворотний рух панорамування
Весь код для анімації буде у вершинному шейдері з уніформою, яка контролює кількість руху. Ми використовуємо уніформи для контролю сили руху, щоб ви могли налаштувати анімацію в редакторі та побачити результати в режимі реального часу, без необхідності перекомпілювати шейдер.
Усі рухи виконуватимуться за допомогою косинусних хвиль, застосованих до VERTEX у просторі моделі. Ми хочемо, щоб вершини були в просторі моделі, щоб рух завжди відбувався відносно орієнтації риби. Наприклад, з боку в бік риба завжди буде рухатися вперед і назад у напрямку зліва направо, а не по осі x у світовій орієнтації.
Щоб контролювати швидкість анімації, ми почнемо з визначення нашої власної змінної часу за допомогою TIME.
//time_scale is a uniform float
float time = TIME * time_scale;
Перший рух, який ми здійснимо, це рух із боку в бік. Це можна зробити шляхом зсуву VERTEX.x на cos TIME. Кожного разу, коли сітка візуалізується, усі вершини будуть переміщатися вбік на величину cos(time).
//side_to_side is a uniform float
VERTEX.x += cos(time) * side_to_side;
Отримана анімація має виглядати приблизно так:
Далі ми додаємо опору. Оскільки риба знаходиться в центрі (0, 0), все, що нам потрібно зробити, це помножити VERTEX на матрицю обертання, щоб вона оберталася навколо центру риби.
Будуємо матрицю обертання так:
//angle is scaled by 0.1 so that the fish only pivots and doesn't rotate all the way around
//pivot is a uniform float
float pivot_angle = cos(time) * 0.1 * pivot;
mat2 rotation_matrix = mat2(vec2(cos(pivot_angle), -sin(pivot_angle)), vec2(sin(pivot_angle), cos(pivot_angle)));
Потім ми застосовуємо його до осей x і z, помноживши його на VERTEX.xz.
VERTEX.xz = rotation_matrix * VERTEX.xz;
Застосувавши лише опору, ви повинні побачити щось на зразок цього:
Наступними двома рухами потрібно панорамувати вниз по хребту риби. Для цього нам потрібна нова змінна, body. body - це поплавок, який має 0 у хвості риби та 1 у її голові.
float body = (VERTEX.z + 1.0) / 2.0; //for a fish centered at (0, 0) with a length of 2
Наступний рух — це косинусова хвиля, яка рухається вниз по довжині риби. Щоб змусити його рухатися вздовж хребта риби, ми зміщуємо вхідні дані для cos на позицію вздовж хребта, яка є змінною, яку ми визначили вище, body.
//wave is a uniform float
VERTEX.x += cos(time + body) * wave;
Це дуже схоже на рух із боку в бік, який ми визначили вище, але в цьому випадку, використовуючи body to offset cos, оскільки кожна вершина вздовж хребта має інше положення в хвилі, що робить її схожою на хвиля рухається уздовж риби.
Останнім рухом є поворот, який являє собою перекат уздовж хребта. Подібно до опорної точки, ми спочатку будуємо матрицю обертання.
//twist is a uniform float
float twist_angle = cos(time + body) * 0.3 * twist;
mat2 twist_matrix = mat2(vec2(cos(twist_angle), -sin(twist_angle)), vec2(sin(twist_angle), cos(twist_angle)));
Ми застосовуємо обертання по осях xy так, щоб риба ніби оберталася навколо свого хребта. Щоб це спрацювало, хребет риби має бути зосереджений на осі z.
VERTEX.xy = twist_matrix * VERTEX.xy;
Ось нанесена риба з твіст:
Якщо ми застосовуємо всі ці рухи один за одним, ми отримаємо текучий желеподібний рух.
Звичайна риба плаває переважно задньою половиною тіла. Відповідно, нам потрібно обмежити рухи панорамування задньою половиною риби. Для цього ми створюємо нову змінну mask.
mask — це поплавок, який переходить від 0 спереду риби до 1 в кінці, використовуючи smoothstep для контролю точки, в якій відбувається перехід від 0 до 1.
//mask_black and mask_white are uniforms
float mask = smoothstep(mask_black, mask_white, 1.0 - body);
Нижче наведено зображення риби з маскою, яка використовується як КОЛЬОР:
Для хвилі ми множимо рух на маску, що обмежить його задньою половиною.
//wave motion with mask
VERTEX.x += cos(time + body) * mask * wave;
InЩоб застосувати маску до повороту, ми використовуємо mix. mix дозволяє нам змішувати положення вершини між повністю повернутою вершиною та не повернутою. Нам потрібно використовувати mix замість множення mask на повернуту VERTEX, оскільки ми не додаємо рух до VERTEX, а замінюємо VERTEX повернутою версією. Якби ми помножили це на mask, ми б зменшили рибу.
//twist motion with mask
VERTEX.xy = mix(VERTEX.xy, twist_matrix * VERTEX.xy, mask);
Поєднання чотирьох рухів разом дає нам остаточну анімацію.
Грайте з уніформою, щоб змінити плавальний цикл риби. Ви побачите, що за допомогою цих чотирьох рухів можна створювати різноманітні стилі плавання.
Виготовлення зграї рибок
Godot дозволяє легко відтворювати тисячі однакових об’єктів за допомогою вузла MultiMeshInstance3D.
Вузол MultiMeshInstance3D створюється та використовується так само, як і вузол MeshInstance3D. У цьому підручнику ми назвемо вузол MultiMeshInstance3D School, оскільки він міститиме зграю риб.
Коли у вас є MultiMeshInstance3D, додайте MultiMesh, а до цього MultiMesh додайте свій Mesh з шейдером вище.
MultiMeshes малює ваш Mesh з трьома додатковими властивостями для кожного екземпляра: Transform (обертання, переклад, масштаб), Color і Custom. Custom використовується для передачі 4 багаторазових змінних за допомогою Color.
instance_count визначає, скільки екземплярів сітки ви хочете намалювати. Наразі залиште instance_count на 0, оскільки ви не можете змінити будь-які інші параметри, поки instance_count більший за 0. Пізніше ми встановимо підрахунок екземплярів у GDScript.
transform_format визначає, 3D чи 2D трансформації. Для цього підручника виберіть 3D.
Для color_format і custom_data_format ви можете вибрати між None, Byte і Float. Немає означає, що ви не передаватимете ці дані (або змінну COLOR для кожного екземпляра, або INSTANCE_CUSTOM) до шейдера. Byte означає, що кожне число, що входить до складу переданого вами кольору, зберігатиметься з 8 бітами, тоді як Float означає, що кожне число зберігатиметься у вигляді числа з плаваючою комою (32 біти). Float повільніший, але більш точний, Byte займе менше пам'яті та буде швидшим, але ви можете побачити деякі візуальні артефакти.
Тепер встановіть instance_count на кількість риби, яку ви хочете мати.
Далі нам потрібно встановити перетворення для кожного екземпляра.
Існує два способи встановлення трансформацій для кожного екземпляра для MultiMeshes. Перший повністю знаходиться в редакторі та описаний у MultiMeshInstance3D tutorial.
Другий полягає в тому, щоб зациклити всі екземпляри та встановити їх перетворення в коді. Нижче ми використовуємо GDScript для циклічного перегляду всіх екземплярів і встановлення їхнього перетворення у випадкову позицію.
for i in range($School.multimesh.instance_count):
var position = Transform3D()
position = position.translated(Vector3(randf() * 100 - 50, randf() * 50 - 25, randf() * 50 - 25))
$School.multimesh.set_instance_transform(i, position)
Запуск цього сценарію розмістить рибу у випадкових позиціях у рамці навколо положення MultiMeshInstance3D.
Примітка
Якщо продуктивність є проблемою для вас, спробуйте запустити сцену з меншою кількістю риби.
Помітили, що всі риби знаходяться в однаковому положенні під час свого плавального циклу? Це робить їх дуже роботизованими. Наступний крок — надати кожній рибі інше положення в плавальному циклі, щоб уся зграя виглядала більш органічно.
Анімація зграї риб
Однією з переваг анімації риби за допомогою функцій cos є те, що вони анімуються за допомогою одного параметра, time. Щоб надати кожній рибі унікальну позицію в циклі плавання, нам потрібно лише зсунути час.
Ми робимо це, додаючи для кожного екземпляра користувацьке значення INSTANCE_CUSTOM до time.
float time = (TIME * time_scale) + (6.28318 * INSTANCE_CUSTOM.x);
Далі нам потрібно передати значення в INSTANCE_CUSTOM. Ми робимо це, додаючи один рядок у цикл for зверху. У циклі for ми призначаємо кожному екземпляру набір із чотирьох випадкових чисел з плаваючою точкою для використання.
$School.multimesh.set_instance_custom_data(i, Color(randf(), randf(), randf(), randf()))
Тепер усі риби мають унікальні позиції в плавальному циклі. Ви можете надати їм трохи більше індивідуальності, використовуючи INSTANCE_CUSTOM, щоб змусити їх плавати швидше або повільніше, помноживши на TIME.
//set speed from 50% - 150% of regular speed
float time = (TIME * (0.5 + INSTANCE_CUSTOM.y) * time_scale) + (6.28318 * INSTANCE_CUSTOM.x);
Ви навіть можете поекспериментувати зі зміною кольору кожного екземпляра так само, як ви змінювали спеціальне значення кожного екземпляра.
Одна проблема, з якою ви зіткнетеся на цьому етапі, полягає в тому, що риби живі, але вони не рухаються. Ви можете переміщувати їх, оновлюючи трансформацію кожного екземпляра для кожної риби в кожному кадрі. Незважаючи на те, що це буде швидше, ніж переміщення тисяч MeshInstance3D за кадр, це, швидше за все, буде повільним.
У наступному посібнику ми розповімо, як використовувати GPUParticles3D, щоб скористатися перевагами графічного процесора та переміщувати кожну рибу окремо, зберігаючи переваги інстансування.