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.

Анимация тысяч рыб с помощью MultiMeshInstance3D

В этом уроке рассматривается метод, используемый в игре ABZU для рендеринга и анимации тысяч рыб с использованием вершинной анимации и статического создания экземпляров сетки.

В Godot это можно сделать с помощью пользовательского шейдера Shader и MultiMeshInstance3D. Используя следующую технику, вы можете визуализировать тысячи анимированных объектов даже на слабом оборудовании.

Начнём с анимации одной рыбы. Затем посмотрим, как распространить эту анимацию на тысячи рыб.

Анимация одной рыбы

Начнём с одной рыбы. Загрузите модель рыбы в MeshInstance3D и добавьте новый ShaderMaterial.

Вот рыба, которую мы будем использовать для примеров изображений. Вы можете использовать любую понравившуюся вам модель рыбы.

../../../_images/fish.png

Примечание

Модель рыбы в этом руководстве создана QuaterniusDev и распространяется по лицензии Creative Commons. CC0 1.0 Universal (CC0 1.0) Передача в общественное достояние https://creativecommons.org/publicdomain/zero/1.0/

Обычно для анимации объектов используются кости и Skeleton3D. Однако кости анимируются на центральном процессоре, поэтому приходится рассчитывать тысячи операций в каждом кадре, и работать с тысячами объектов становится невозможно. Используя вершинную анимацию в вершинном шейдере, вы избегаете использования костей и можете вместо этого рассчитать всю анимацию всего за несколько строк кода на графическом процессоре.

Анимация будет состоять из четырех ключевых движений:

  1. Движение из стороны в сторону

  2. Поворотное движение вокруг центра рыбы

  3. Панорамирование волнового движения

  4. Панорамирование с поворотным движением

Весь код анимации будет находиться в вершинном шейдере, а юниформы будут управлять интенсивностью движения. Мы используем юниформы для управления силой движения, чтобы вы могли настраивать анимацию в редакторе и видеть результаты в реальном времени, без необходимости перекомпиляции шейдера.

Все движения будут осуществляться с помощью косинусоидальных волн, приложенных к 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;

Полученная анимация должна выглядеть примерно так:

../../../_images/sidetoside.gif

Затем добавляем точку опоры. Поскольку центр рыбы находится в точке (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;

С применением только оси вращения вы должны увидеть что-то вроде этого:

../../../_images/pivot.gif

Следующие два движения должны панорамировать рыбу вдоль позвоночника. Для этого нам понадобится новая переменная 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, каждая вершина вдоль позвоночника имеет различное положение в волне, что создает впечатление, будто волна движется вдоль рыбы.

../../../_images/wave.gif

Последнее движение — это скручивание, то есть панорамирование вдоль позвоночника. Как и в случае с поворотом, сначала строим матрицу вращения.

//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;

Вот рыба, подвергнутая закручиванию:

../../../_images/twist.gif

Если применить все эти движения одно за другим, то получится жидкое желеобразное движение.

../../../_images/all_motions.gif

Обычные рыбы плавают преимущественно задней половиной тела. Соответственно, нам нужно ограничить панорамирование только задней половиной тела. Для этого создаём новую переменную 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:

../../../_images/mask.png

Для волны мы умножаем движение на 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);

Объединив четыре движения, мы получаем итоговую анимацию.

../../../_images/all_motions_mask.gif

Продолжайте экспериментировать с формой тела, чтобы изменить цикл плавания рыбы. Вы обнаружите, что, используя эти четыре движения, можно создавать самые разные стили плавания.

Создание косяка рыб

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, чтобы воспользоваться преимуществами графического процессора и перемещать каждую рыбу по отдельности, при этом получая преимущества инстансинга.