Work in progress
The content of this page was not yet updated for Godot
4.6
and may be outdated. If you know how to improve this page or you can confirm
that it's up to date, feel free to open a pull request.
Анимация тысяч рыб с помощью MultiMeshInstance3D
В этом уроке рассматривается метод, используемый в игре ABZU для рендеринга и анимации тысяч рыб с использованием вершинной анимации и статического создания экземпляров сетки.
В Godot это можно сделать с помощью пользовательского шейдера Shader и MultiMeshInstance3D. Используя следующую технику, вы можете визуализировать тысячи анимированных объектов даже на слабом оборудовании.
Начнём с анимации одной рыбы. Затем посмотрим, как распространить эту анимацию на тысячи рыб.
Анимация одной рыбы
Начнём с одной рыбы. Загрузите модель рыбы в MeshInstance3D и добавьте новый ShaderMaterial.
Вот рыба, которую мы будем использовать для примеров изображений. Вы можете использовать любую понравившуюся вам модель рыбы.
Примечание
Модель рыбы в этом руководстве создана QuaterniusDev и распространяется по лицензии Creative Commons. CC0 1.0 Universal (CC0 1.0) Передача в общественное достояние https://creativecommons.org/publicdomain/zero/1.0/
Обычно для анимации объектов используются кости и Skeleton3D. Однако кости анимируются на центральном процессоре, поэтому приходится рассчитывать тысячи операций в каждом кадре, и работать с тысячами объектов становится невозможно. Используя вершинную анимацию в вершинном шейдере, вы избегаете использования костей и можете вместо этого рассчитать всю анимацию всего за несколько строк кода на графическом процессоре.
Анимация будет состоять из четырех ключевых движений:
Движение из стороны в сторону
Поворотное движение вокруг центра рыбы
Панорамирование волнового движения
Панорамирование с поворотным движением
Весь код анимации будет находиться в вершинном шейдере, а юниформы будут управлять интенсивностью движения. Мы используем юниформы для управления силой движения, чтобы вы могли настраивать анимацию в редакторе и видеть результаты в реальном времени, без необходимости перекомпиляции шейдера.
Все движения будут осуществляться с помощью косинусоидальных волн, приложенных к 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 для смещения 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);
Ниже представлено изображение рыбы с mask, использованной в качестве COLOR:
Для волны мы умножаем движение на mask, которая ограничит его задней половиной.
//wave motion with mask
VERTEX.x += cos(time + body) * mask * wave;
Чтобы применить маску к повороту, мы используем 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 рисуют вашу сетку с тремя дополнительными свойствами для каждого экземпляра: Transform (вращение, перемещение, масштаб), Color и Custom. Custom используется для передачи четырёх многоцелевых переменных с помощью Color.
instance_count определяет количество экземпляров сетки, которые нужно отрисовать. Пока оставьте instance_count равным 0, поскольку вы не сможете изменить другие параметры, пока instance_count больше 0. Мы установим instance count в GDScript позже.
transform_format определяет, являются ли используемые преобразования трёхмерными или двумерными. В этом руководстве выберите 3D.
Как для color_format, так и для custom_data_format вы можете выбрать между None, Byte и Float. None означает, что вы не будете передавать эти данные (либо переменную COLOR для каждого экземпляра, либо INSTANCE_CUSTOM) шейдеру. Byte означает, что каждое число, составляющее цвет, который вы передаете, будет храниться с 8 битами, в то время как Float означает, что каждое число будет храниться в числе с плавающей запятой (32 бита). Float медленнее, но точнее, Byte займет меньше памяти и будет быстрее, но вы можете увидеть некоторые визуальные артефакты.
Теперь установите instance_count на желаемое количество рыб.
Далее нам необходимо настроить преобразования для каждого экземпляра.
Существует два способа настройки преобразований для каждого экземпляра MultiMesh. Первый способ полностью реализован в редакторе и описан в 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. Чтобы задать каждой рыбе уникальное положение в цикле плавания, нам достаточно задать смещение 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, чтобы воспользоваться преимуществами графического процессора и перемещать каждую рыбу по отдельности, при этом получая преимущества инстансинга.