Анимация с помощью шейдера в Unity

В последнее время я работал над эффектом возрождения главного героя в моей игре King, Witch and Dragon. Для этого мне понадобилась пара сотен анимированных крыс.

Создавать на сцене несколько сотен Skinned Mesh со скелетной анимацией ради одного эффекта крайне нецелесообразно, поэтому я решил использовать систему частиц (или «партиклов»). Для этого пришлось прибегнуть к альтернативному способу анимации.

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

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

Анимация, которую я собираюсь детально разобрать, выглядит так:

Подготовка

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

Анимация с помощью шейдера в Unity

Для разделения модели на разные «зоны» (туловище, лапы, хвост) я использовал UV-координаты vertex. Поэтому развёртка у модели немного специфичная.

Анимация с помощью шейдера в Unity

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

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

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

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

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

FBX-модель крысы с развёрткой и diffuse-текстуру можно скачать отсюда.

Создаём шейдер

Для наглядности я сначала соберу шейдер в нодовом Shader Graph, а потом покажу его текстовую версию.

Создаём Unlit Graph, в качестве Preview выбираем Custom Mesh и выбираем нашу модель крысы.

Анимация с помощью шейдера в Unity

Применяем текстуру

Создаём параметр Texture 2D, который будет нашей основной текстурой. Создаём ноду Sample Texture 2D, в качестве текстуры задаём наш параметр и подсоединяем ноду к полю Color нашей мастер ноды.

Анимация с помощью шейдера в Unity

Далее мы будем работать только с vertex модели.

Основное волнообразное движение

Создавать его мы будем с помощью синусоидальной волны. Для создания и настройки основного прыжка нам понадобится три параметра:

  • Jump Amplitude — как высоко будет прыгать крыса;
  • Jump Frequency — как часто она будет это делать;
  • Jump Speed — как быстро будет происходить вертикальное смещение.
Анимация с помощью шейдера в Unity

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

Умножив ноду Time на параметр Jump Speed мы сможем контролировать с какой скоростью будет смещаться наша синусоидальная функция и, соответственно, с какой скоростью будет меняться вертикальное смещение vertex’ов.

Умножив горизонтальную составляющую UV на параметр Jump Frequency, мы сможем контролировать сжатие и растяжение синусоидальной функции, а значит, как часто будет прыгать наша крыса.

Сложив эти 2 произведения и подсоединив результат к ноде Sin, мы получим желаемую форму синусоидальной волны. По умолчанию функция принимает значения между -1.0 и 1.0, поэтому, если мы помножим нашу функцию на параметр Jump Amplitude, мы получим желаемую траекторию.

Анимация с помощью шейдера в Unity

Теперь нужно применить результат к положению vertex, но сделать это нужно только для вертикальной составляющей. Поэтому мы возьмём ноду Position, разделим её на компоненты с помощью ноды Split, добавим значение, полученное из синусоидальной функции к вертикальной компоненте, и соберем обратно, с помощью ноды Combine. После этого подсоединяем результат к Vertex Position нашей мастер-ноды.

Анимация с помощью шейдера в Unity

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

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

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

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

Добавляем ноду в наш граф.

Анимация с помощью шейдера в Unity

Теперь анимация крысы выглядит так.

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

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

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

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

Граф теперь выглядит так.

Анимация с помощью шейдера в Unity

Крыса теперь остаётся некоторое время на земле перед прыжком.

Дополнительное движение хвоста

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

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

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

Мы будет использовать ноду Smooth Step с горизонтальной компонентой UV в качестве in-значения.

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

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

Анимация с помощью шейдера в Unity
Анимация с помощью шейдера в Unity

Теперь движение хвоста крысы стало более заметно (значение параметра Tail Extra Swing = 0,3):

Движение лап

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

  • Legs Amplitude — насколько сильно вперёд и назад будут смещаться лапы;
  • Legs Frequency — как часто они это будут делать.

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

Так как лапы будут двигаться вперёд и назад, нам не понадобится нода Absolute для синусоидальной функции.

Анимация с помощью шейдера в Unity

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

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

Для этого мы снова будем использовать ноду Smooth Step, но теперь в качестве in-параметра будем использовать вертикальную ось UV-развёртки. Обозначим градиент между значениями 0,1 и 0,4.

Почему 0,1 а не 0,0? Чтобы не деформировались стопы, которые находятся в нижней части развёртки. Для всех vertex, находящихся ниже уровня 0,1, смещение будет одинаковым.

Анимация с помощью шейдера в Unity

Давайте теперь добавим полученное значение к Z-координате vertex (продольная ось), временно отключим движение туловища и посмотрим на изолированное движение лап.

Анимация с помощью шейдера в Unity

Теперь давайте соединим всё вместе. Я специально выставил скорость поменьше, чтобы было проще заметить недочёты.

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

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

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

Анимация с помощью шейдера в Unity

Подобрав нужное значение фазового смещения (в моём случае -1,0), получаем такую анимацию.

И с нормальной скоростью.

Финальный граф целиком.

Анимация с помощью шейдера в Unity

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

Для тех, кто ещё не перешёл на URP/HDRP в Unity или просто предпочитает писать шейдеры в текстовом редакторе, вот текстовая версия того, что мы только что сделали.

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 } } }

Сложности с системой частиц

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

Этот шейдер рассчитан на работу в Object space (локальном пространстве объекта). Если вы назначите его на GameObject, всё будет работать отлично. Но если назначить его системе частиц, начнутся артефакты.

Насколько я знаю (тут могу ошибаться), раньше партиклы в системе частиц тоже работали в локальном пространстве, но несколько версий назад их перевели в World Space, чтобы они батчились с целью экономии ресурсов. Поэтому прибавление значений к координатам Y и Z будет смещать vertex в мировом пространстве, соответственно все они будут смещаться в одном и том же направлении, независимо от поворота крысы.

Чтобы решить эту проблему я использовал 2 Custom Vertex Stream в системе частиц и передавал туда из скрипта значения up-vector и forward-vector для каждой частицы. Первый учитывался для рассчета направления смещения тела во время анимации прыжка, второй для направления смещения лап.

Заключение

Мы создали 2 версии шейдера, который позволяет анимировать крысу без использования скелета и keyframe-анимации.

Такие анимации подойдут для фоновых и декоративных объектов, которые не являются основным фокусом внимания, но добавляют картинке живости и динамики.

Используя показанные техники можно анимировать различных простых существ, особенно если их должно быть много (косяк рыб, стая птиц, толпа крыс). Надеюсь, было полезно и интересно.

Чтобы поддержать разработку игры, добавляйте King, Witch and Dragon в вишлис на Steam. Если хотите принять участие в обсуждении, вступайте в группу ВК, а также подписывайтесь на меня в Twitter и Instagram.

Спасибо за внимание!

#лонг #unity #графика #опыт #анимация

705705
113 комментария

Не всё понятно, но очень интересно.

77

С шейдерами всегда так 

34

Кому интересны более продвинутые техники с вертексной анимацией, для UE4 есть тулзы для конвертации кейфреймов в вертексную анимацию:  

12

Если запустить гифку обратно, то чувака съедает толпа крыс.

16

На крыс из Plague Tale похоже

13