[Перевод] Создание шейдерной анимации в Unity

Недавно я работал над анимацией респауна и спецэффектом главного героя моей игры «King, Witch and Dragon». Для этого спецэффекта мне нужна была пара сотен анимированных крыс.


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

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

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

Я подробно опишу создание вот этой анимации:

Rat animation


Для начала я создал в Blender низкополигональную модель крысы.

Rat model


Для разделения модели на разные части (тело, хвост, лапы) я использую UV-координаты. Чтобы всё работало как нужно, модель нужно развернуть особым образом.

Rat UV


Основное движение крысы — это прыжки. Чтобы все вершины анимировались плавно и синхронно, нам нужно разместить всё тело крысы вдоль одной из осей. Для удобства я разместил её вдоль горизонтальной оси U.

Глаза, внутренняя часть уха и нос должны иметь другой цвет на текстуре, поэтому я разместил их отдельно, но выполнил смещение только по оси V, не меняя горизонтальных координат. Если переместить их по горизонтальной оси, то их движение не будет синхронизировано с движением головы.

Хвост крысы занимает левую половину развёртки (координаты U от 0,0 до 0,5). Это будет нашей маской хвоста, которую мы используем для анимации хвоста.

Лапы расположены в нижней половине развёртки (координаты V от 0,0 до 0,4). Это наша маска лап. Кроме того, лапы сжаты по горизонтальной оси U, чтобы предотвратить нежелательную деформацию при движении вперёд-назад. Так как в своём проекте я использую cel-shading без детализованной текстуры, сжатые UV не вызовут проблем.

На основании этой UV-развёртки я создал диффузную текстуру. Теперь мы можем начать работу над шейдером.

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


Сначала я создам этот шейдер в Shader Graph движка Unity, а затем покажу текстовую версию.

Давайте создадим Unlit Graph, в качестве Preview выберем Custom Mesh, а затем выберем модель крысы.

c8e2acecba831ae35a7dc07ca02372f7.png


Накладываем текстуру


Создадим новый параметр Texture 3D, это будет наша основная диффузная текстура. Создадим нод Sample Texture 2D, соединим наш новый параметр с его полем texture, а затем соединим нод с полем Color нода Master.

5017f67aff9b9decf35af1ff5efea52b.png


В дальнейшем мы будем работать только с вершинами.

Основное движение


Мы создадим его при помощи синусоиды. Чтобы настроить форму волны, нам нужны три параметра:

  • Jump Amplitude — высота прыжков крысы
  • Jump Frequency — частота прыжков крысы
  • Jump Speed — скорость движения вдоль вертикальной оси


e247895ba93a9956c182afc7b9d6a2ae.jpg


Два основных нода, которые позволят нам создать анимированное движение — это Time и UV. В ноде UV нам нужно будет использовать каждую ось по отдельности, поэтому мы соединим его с нодом Split, который даст нам доступ к каждому из каналов.

Мы можем управлять скоростью прокрутки синусоиды, умножив нод Time на параметр Jump Speed.

Если умножить горизонтальный компонент UV на Jump Frequency, то мы сможем управлять сжатием и растягиванием синусоиды.

Сумма этих произведений даст нам синусоиду нужной формы. По умолчанию синусоида возвращает значения в интервале от -1,0 до 1,0. Если мы умножим их на Jump Amplitude, то получим траекторию прыжка.

Building sive wave


Теперь нам нужно применить результат к позициям вершин, но только к их вертикальной компоненте (локальной оси Y). Мы используем нод Position и соединим его с нодом Split. Затем прибавим значение синусоиды к компоненте Y и соберём всё вместе при помощи нода Combine. Подключим выход этого нода к полю Vertex Position нода Master.

Apply Y-offset


Крыса начала двигаться, но не совсем так, как нам нужно. Она больше походит на дельфина, чем на крысу.

Rat sine wave


Как говорилось выше, синусоида может возвращать как положительные, так и отрицательные значения. Если мы теперь поместим крысу на поверхность, она будет «нырять» в неё. Кроме того, у синусоиды очень плавные экстремумы, из-за чего анимация походит на плавание.

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

Absolute values of the sine wave


Сверху — обычные значения, снизу — абсолютные.

Давайте добавим этот нод к нашему графу.

Graph absolute node


Теперь анимация стала более скачущей.

Rat absolute sine wave


Это уже больше похоже на прыжки или скачки, но всё равно далеко от желаемого. Сейчас проблема заключается в том, что как только лапы касаются земли, они отскакивают обратно в воздух. Это логично с точки зрения формы синусоиды, но неестественно с точки зрения движения крысы. Давайте будем какое-то время удерживать лапы крысы на земле перед следующим прыжком.

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

Sine wave vertical offset


Сверху — абсолютное значение, посередине — со смещением, внизу — максимум между синусом и нулём.

Чтобы реализовать это в графе, нам нужен новый параметр Jump Vertical Offset, который позволит нам настраивать величину сдвига синусоиды.

Graph abs sine offset


Теперь крыса может какое-то время оставаться на земле.

Rat jump with vertical offset


Дополнительное раскачивание хвоста


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

Мы используем UV-координаты для маскировки хвоста и анимирования его отдельно от тела.

Мы знаем, что хвост расположен в левой половине UV-развёртки. Создадим плавный градиент от 0,0 до 0,5 (или даже до 0,6, чтобы эффект был более плавным) по горизонтальной оси U. В 0,0 у нас будет белый цвет, в 0,6 и далее — чёрный. Чем ярче пиксель градиента, тем больше дополнительного движения прикладывается к вершине. По сути, на кончик хвоста он будет влиять сильнее всего, а ближе к телу эффект будет ослабевать.

Для создания этого градиента мы используем нод Smooth Step.

Также нам потребуется новый параметр Tail Extra Swing для задания величины дополнительного движения.

Умножив этот новый параметр на выход нода Smooth Step, мы получим распределение движения вдоль хвоста. Затем мы прибавим его к параметру Jump Amplitude, чтобы получить окончательное движение тела, учитывающее дополнительное раскачивание хвоста.

Graph tail mask


Graph tail mask full


Теперь движение хвоста более заметно (Tail Extra Swing = 0.3).

Rat extra tail swing


Движение лап


Для движения лап мы используем примерно такую же структуру, что и для тела. Нам потребуется ещё пара новых параметров:

  • Legs Amplitude — величина перемещения лап относительно их позиций по умолчанию
  • Legs Frequency — частота движения лап


Нам не нужен параметр Legs Speed, потому что движение лап должно быть синхронизировано с движением тела, поэтому мы снова используем параметр Jump Speed. Единственное, что здесь стоит учитывать: поскольку мы используем абсолютное значение синусоиды, за один цикл получается два прыжка. Поэтому чтобы компенсировать это, мы используем Jump Speed * 2.

Лапы должны двигаться вперёд и назад (и положительное, и отрицательное смещение), поэтому в этом случае нам не понадобится нод Absolute.

Graph legs sine wave


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

Мы снова используем нод Smooth Step, но на этот раз в качестве входного параметра выберем вертикальную ось UV. Давайте зададим градиент от 0,1 до 0,4.

Почему 0,1, а не 0,0? Чтобы избежать деформации лап. Все вершины ниже уровня 0,1 будут иметь одинаковое смещение.

Нам нужно настроить значение Legs Frequency таким образом, чтобы когда передние лапы идут вперёд, задние двигались назад, и наоборот. В моём случае я задал значение 10.

Graph legs mask


Давайте прибавим результат к локальной позиции вершин по Z. Я временно отключил движение тела, чтобы отдельно посмотреть на анимацию лап.

Graph isolated legs movement


Rat isolated legs movement


Теперь скомбинируем всё вместе и посмотрим, как это выглядит. Я намеренно уменьшил скорость, чтобы легче было находить возможные проблемы.

Rat slomo


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

Чтобы решить эту проблему, создадим новый параметр Legs Phase Offset, позволяющий нам компенсировать эту разность фаз и согласовать анимации.

Чтобы это смещение фаз работало, нужно добавить его в нод Time уже после умножения на Jump Speed (чтобы не изменять скорость), но перед всеми остальными манипуляциями.

Graph phase offset


После настройки значения (я указал -1,0) анимация стала правильной.

Rat with phase offset


Вот как это выглядит при нормальной скорости.

Rat final


Готовый граф:

Complete graph


Текстовая версия шейдера


Для тех, кто пока не перешёл на URP/HDRP или просто предпочитает писать шейдеры вручную, вот версия в коде:

Shader "Unlit/Rat"
{
    Properties
    {
        _JumpSpeed("Jump Speed", float) = 10
        _JumpAmplitude("Jump Amplitude", float) = 0.18
        _JumpFrequency("Jump Frequency", float) = 2
        _JumpVerticalOffset("Jump Vertical Offset", float) = 0.33
        _TailExtraSwing("Tail Extra Swing", float) = 0.15
        _LegsAmplitude("Legs Amplitude", float) = 0.10
        _LegsFrequency("Legs Frequency", float) = 10
        _LegsPhaseOffset("Legs Phase Offset", float) = -1
        [NoScaleOffset]
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            half _JumpSpeed;
            half _JumpAmplitude;
            half _JumpFrequency;
            half _JumpVerticalOffset;
            half _TailExtraSwing;
            half _LegsAmplitude;
            half _LegsFrequency;
            half _LegsPhaseOffset;

            v2f vert (appdata v)
            {
                float bodyPos = max((abs(sin(_Time.y * _JumpSpeed + v.uv.x * _JumpFrequency)) - _JumpVerticalOffset), 0);
                float tailMask = smoothstep(0.6, 0.0, v.uv.x) * _TailExtraSwing + _JumpAmplitude;
                bodyPos *= tailMask;
                v.vertex.y += bodyPos;

                float legsPos = sin(_Time.y * _JumpSpeed * 2 + _LegsPhaseOffset + v.uv.x * _LegsFrequency) * _LegsAmplitude;
                float legsMask = smoothstep(0.4, 0.1, v.uv.y);
                legsPos *= legsMask;
                v.vertex.z += legsPos;

                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}


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

Такой способ подойдёт для фоновых объектов, не находящихся в основном фокусе внимания игрока. Кроме того, по сравнению с рендерерами мешей со скиннингом (Skinned Mesh Renderers), он обеспечивает более высокую производительность.

Надеюсь, он был для вас интересным и/или полезным.

© Habrahabr.ru