Популярные функции для создания реалистичных анимаций.
![](https://habrastorage.org/getpro/habr/upload_files/0c0/e2c/d45/0c0e2cd4584fb0dfc6d054998fd94341.png)
Мы продолжаем цикл статей про математику и смежные дисциплины в компьютерной графике по курсу Александра Паничева, ведущего разработчика логики в UNIGINE. В этот раз поговорим о функциях плавности, которые используются в анимации (и не только).
Простейший пример
Представим ситуацию, что у нас есть машинка и ее нужно анимировать, чтобы она ездила от одного края экрана до другого и обратно.
Дизайнер: «Надо бы анимировать эту машинку!»
![](https://habrastorage.org/getpro/habr/upload_files/c12/830/808/c128308081e38716164da5e81746e952.png)
А теперь представим абстрактного программиста-джуна, который ничего не понимает и решает просто использовать функцию lerp (линейную интерполяцию). Получается нереалистично (1-я анимация ниже), и это замечает Дизайнер, который говорит программисту, что машинка должна в начале разгоняться, а в конце замедляться (2-я анимация ниже):
![](https://habrastorage.org/getpro/habr/upload_files/2cc/a1f/c49/2cca1fc49846175d85114d0cf0927fae.gif)
То есть нужно нелинейное движение (как это обычно и происходит в жизни). При этом, в следующий раз Дизайнер вообще может сказать, что она должна начать ускоряться с 5-й секунды или ехать не более 3 секунд… Можно, конечно, использовать какие-нибудь функции из физики:
![](https://habrastorage.org/getpro/habr/upload_files/c35/013/73f/c3501373f974c99eb156cb78fdfbcd44.png)
С ускорением машины все просто, но как применить эти функции, чтобы машина замедлялась ближе к концу и не проехала дальше нужного?
Опытный программист сразу вспомнит про кривые. Например, про так называемую кривую Безье:
![Кубическая кривая Безье Кубическая кривая Безье](https://habrastorage.org/getpro/habr/upload_files/e0b/3d1/d6f/e0b3d1d6fdab9c3f14c22606c7dde62b.gif)
В интерактивных приложениях мы часто сталкиваемся с двумя видами кривых Безье: квадратичной и кубической.
1D Quadratic Bezier (квадратичная кривая Безье)
float bezier_quad(float p0, float p1, float p2, float t)
{
float it = 1.0f - t;
return p0 * (it * it) +
p1 * (2.0f * it * t) +
p2 * (t * t);
}
![](https://habrastorage.org/getpro/habr/upload_files/63b/c66/bb1/63bc66bb1b4e2c59475f3d844dde0a20.png)
![(7 умножений, 2 сложения, 1 вычитание и 4 аргумента на входе) (7 умножений, 2 сложения, 1 вычитание и 4 аргумента на входе)](https://habrastorage.org/getpro/habr/upload_files/888/e89/902/888e89902413dbae2d57c1923dd9f377.png)
Если воспринимать параметр t за нормализованное время (от 0 до 1), p0 — начальная позиция автомобиля, p2 — конечное, а возвращаемое значение — за текущую позицию автомобиля для текущего времени t, то становится понятно, что, управляя параметром p1 (так называемой контрольной точкой или касательной) мы можем двигать автомобиль либо линейно (p1 между p0 и p2), либо с ускорением (p1 ближе к p0), либо с торможением (p1 ближе к p2).
А как сделать плавное ускорение в начале и плавное торможение в конце?
1D Cubic Bezier (кубическая кривая Безье)
float bezier_cubic(float p0, float p1, float p2, float p3, float t)
{
float tt = t * t;
float it = 1.0f - t;
float itt = it * it;
return p0 * (itt * it) +
p1 * (3.0f * itt * t) +
p2 * (3.0f * it * tt) +
p3 * (tt * t);
}
![](https://habrastorage.org/getpro/habr/upload_files/0e8/e34/833/0e8e34833fc1ca5cc4265722613b4b0a.png)
![(12 умножений, 3 сложения, 1 вычитание и 5 аргументов на входе) (12 умножений, 3 сложения, 1 вычитание и 5 аргументов на входе)](https://habrastorage.org/getpro/habr/upload_files/f88/8ed/76c/f888ed76c948c1efbabe29e65ca6db51.png)
Здесь у нас уже есть целых две контрольных точки! Первое для первой половины анимации и второе для второй половины.
Можно вызвать bezier_cubic(0, 0, 1, 1, t)
и увидеть, что анимация у нас стала той, какую мы хотели видеть.
![](https://habrastorage.org/getpro/habr/upload_files/3b5/ad7/fb4/3b5ad7fb46b456b4f724c39eaa411d75.png)
Ну... Почти. С одномерной кубической Безье невозможно контролировать степень искажения, нельзя сделать ускорение и/или торможение еще более сильным.
Что делать? Можно сделать поверх текущей кривой вторую, которая будет искажать t (таким образом и работают кривые в катсцен-редакторах)... Но посмотрите на сложность функции: здесь используется 12 умножений, 3 сложения, 1 вычитание и 5 аргументов на входе. Нельзя ли как-то... попроще? И чтобы контроля было больше. Чтобы можно было сильнее изгибать кривую.
Конечно можно! Ведь для этого и существуют «функции плавности»!
Функции плавности (Easing Functions) Роберта Пеннера
![Роберт Пеннер и его учебник по Flash Роберт Пеннер и его учебник по Flash](https://habrastorage.org/getpro/habr/upload_files/62b/fff/674/62bfff674a415ece19bb55f00b23e8f5.png)
Такого рода анимации, как описанные выше на примере с машинкой, называют Duration-based. У нас есть начало, есть конец и есть продолжительность.
Линейное движение — это использование функции lerp(), она же линейная интерполяция. Она есть в любых игровых движках.
Нелинейное движение — это использование easing-функций (или функций плавности). Они уже встречаются в движках заметно реже. Например, в движке Unity есть сторонний плагин iTween — он уже много лет в топе по скачиваниям.
Помимо разработчиков игровых движков, функциями плавности пользуются веб-дизайнеры и аниматоры.
Функции плавности придумал Роберт Пеннер — математик, физик и Flash-разработчик. Его раздражало, что во Flash сглаженная интерполяция работала либо на ускорение (Ease от -100 до 0, In), либо на торможение (Ease от 0 до 100, Out). Прямо, как с квадратичной кривой Безье. Из-за этого для создания того примера с разгоняющейся в начале и замедляющейся в конце машинкой приходилось использовать дополнительные ключевые кадры.
Всего Роберт изобрел 30 функций:
![](https://habrastorage.org/getpro/habr/upload_files/ae7/bc4/22b/ae7bc422bb50fea30ef910c5ce3e6bd2.png)
Кажется, что много, но их можно разделить на 3 категории: In (ускоряющие), Out (затухающие) и InOut (ускорение с затуханием в конце):
![](https://habrastorage.org/getpro/habr/upload_files/8a2/8af/6c8/8a28af6c82a1ed79e7597b10d8e10ceb.png)
Рассмотрим самую первую функцию easeInQuad. Выглядит она так:
float easeInQuad(float t)
{
return t * t;
}
t — это нормализованное время от 0 до 1 (прямо, как в lerp). Выходящее значение — полученная кривая, также в пределах от 0 до 1 (но есть и исключения, например easeOutElastic).
Как можно понять, Quad тут написано не просто так: аргумент t мы возводим в квадрат. Последующие три функции Cubic, Quart, Quint делают, соответственно, похожую вещь.
Если линейная интерполяция в данном случае — это return t, то остальные варианты — это различные степени t: 3-я, 4-ая, 5-ая. Это и дает изгиб линии — чем выше степень, тем сильнее:
![](https://habrastorage.org/getpro/habr/upload_files/9ae/ad3/926/9aead3926e5a6d5886ccc400ce0ec532.png)
Следующие — Sine, Expo и Circ — это синус/косинус, возведение в степень и движение по окружности соответственно.
![](https://habrastorage.org/getpro/habr/upload_files/457/33b/a4c/45733ba4c96bf98ae8c9c2f94e4d8d4e.png)
Последняя группа — самые необычные и часто используемые: Elastic (пружина), InBack / OutBack (уход за границу значений от 0 до 1), Bounce (скачки мяча и т.п.).
Функции плавности — это необязательно только положение или смещение. Это также может быть масштаб, поворот, изменение цвета и даже искажение времени — все, что имеет движение можно помножить на эти функции плавности.
EaseInQuad, EaseOutQuad, EaseInOutQuad
На примере анимации движения, вращения и масштабирования easeInQuad() выглядит так:
![EaseInQuad (на PICO-8 by ValerADHD) EaseInQuad (на PICO-8 by ValerADHD)](https://habrastorage.org/getpro/habr/upload_files/79f/52a/409/79f52a40924a4082c0c366e16505c653.gif)
Out-вариант данной функции выглядит так:
float easeOutQuad(float x)
{
float z = 1 - x;
return 1 - z * z;
}
Результат:
![EaseOutQuad (на PICO-8 by ValerADHD) EaseOutQuad (на PICO-8 by ValerADHD)](https://habrastorage.org/getpro/habr/upload_files/fc3/257/442/fc3257442464190c029a8ccbdeb3b750.gif)
А это уже совмещение двух предыдущих графиков:
float easeInOutQuad(float x)
{
if (x < 0.5f)
return 2.0f * x * x;
else
{
float z = 1 - x;
return 1 - 2.0f * z * z;
}
}
Результат:
![EaseInOutQuad (на PICO-8 by ValerADHD) EaseInOutQuad (на PICO-8 by ValerADHD)](https://habrastorage.org/getpro/habr/upload_files/517/e58/1da/517e581daae0ab37f48c36d208def680.gif)
EaseInQuad, EaseInCubic, EaseInQuart, EaseInQuint
//Функция EaseInQuad
float easeInQuad(float x)
{
return x * x;
}
//Функция EaseInCubic
float easeInCubic(float x)
{
return x * x * x;
}
//Функция EaseInQuart
float easeInQuart(float x)
{
return x * x * x * x;
}
//Функция EaseInQuint
float easeInQuint(float x)
{
return x * x * x * x * x;
}
Как можно увидеть ниже, квадратичная функция (во 2-й степени) — самая плавная, но и самая медленная. А функция в 5-й степени — самая быстрая, но и наименее плавная:
![](https://habrastorage.org/getpro/habr/upload_files/347/ca6/7f2/347ca67f2010d160e3bf5bf35a92078d.gif)
EaseInSine, EaseInExpo, EaseInCirc
//Функция EaseInQuint
float easeInSine(float x)
{
return 1 - cos((x * PI) / 2);
}
//Функция EaseInExpo
float easeInExpo(float x)
{
if (x == 0)
return 0;
return pow(2, 10 * x - 10);
}
//Функция EaseInCirc
float easeInCirc(float x)
{
return 1 - sqrt(1 - x * x);
}
А вот как выглядят результаты функций с косинусом, возведением в степень и движением по окружности:
![](https://habrastorage.org/getpro/habr/upload_files/ccf/4ac/fb0/ccf4acfb071fb75259d3d394d5fd0cee.gif)
EaseOutBack, EaseOutElastic, EaseOutBounce
float easeOutBack(float x)
{
const float c1 = 1.70158f;
const float c3 = c1 + 1;
const float z = x - 1;
return 1 + c3 * z*z*z + c1 * z*z;
}
Результат:
![](https://habrastorage.org/getpro/habr/upload_files/28f/cdc/34c/28fcdc34c15d076c959ffe1d1e7aefcb.gif)
float easeOutElastic(float x)
{
if (x == 0 || x == 1)
return x;
const float c4 = (2 * Math.PI) / 3;
return
pow(2, -10 * x) *
sin((x * 10 - 0.75f) * c4) + 1;
}
Результат:
![](https://habrastorage.org/getpro/habr/upload_files/088/244/180/088244180b5559f02f1645ac9ed82154.gif)
float easeOutBounce(float x)
{
const float n1 = 7.5625f;
const float d1 = 2.75f;
if (x < 1 / d1)
return n1 * x * x;
else if (x < 2 / d1)
return n1 * (x -= 1.5f / d1) * x + 0.75f;
else if (x < 2.5f / d1)
return n1 * (x -= 2.25f / d1) * x + 0.9375f;
else
return n1 * (x -= 2.625f / d1) * x + 0.984375f;
}
Результат:
![](https://habrastorage.org/getpro/habr/upload_files/a2c/959/04c/a2c95904c24c5b6880da593e56703298.gif)
Remapping Function
Функции плавности Роберта Пеннера подразумевают, что входной аргумент нормализован (имеет значение от 0 до 1). Выходное значение так же в этих же пределах (почти всегда). Поэтому их часто используют вместе с функцией изменения диапазона:
float remap(float value, float low1, float high1, float low2, float high2)
{
return low2 + (value - low1) * (high2 - low2) / (high1 - low1);
}
Указываем диапазон, например, от 5 до 10. Если взять число ровно посередине — 7,5, то после изменения диапазона на от 1 до 3 число тоже будет ровно посередине — 2.
![remap(7.5f, 5, 10, 1, 3) -> 2 remap(7.5f, 5, 10, 1, 3) -> 2](https://habrastorage.org/getpro/habr/upload_files/1e7/a69/cff/1e7a69cff39e3a860a8afe6b5edb9e46.png)
Как работать вместе с easing functions?
Допустим, нам нужна анимация от 5-ой до 7-ой секунды. В этот период надо распахнуть дверь с 0 до 90 градусов:
![](https://habrastorage.org/getpro/habr/upload_files/2d0/bee/907/2d0bee907b3bf1d60e5b811d141a1839.png)
time_n = saturate(remap(time, 5, 7, 0, 1));
door_angle = remap(easeOutBack(time_n), 0, 1, 0, 90);
Примеры реализации
Функции плавности часто используются в различных интерфейсах, вроде сайтов или мобильных приложений.
![Плавные переходы баннеров на сайтах Плавные переходы баннеров на сайтах](https://habrastorage.org/getpro/habr/upload_files/d21/c06/211/d21c0621178aeb13ae45b43626f991c2.gif)
![Плавное переключение интерфейсов на смартфоне Плавное переключение интерфейсов на смартфоне](https://habrastorage.org/getpro/habr/upload_files/f62/37a/bb8/f6237abb850c980cdf8deb827dd20805.gif)
![Плавное открытие заметки в приложении Плавное открытие заметки в приложении](https://habrastorage.org/getpro/habr/upload_files/69e/0c4/921/69e0c492101444411e179304248bea40.gif)
![Реагирующие на курсор мыши кнопки, за счет функции Elastic Реагирующие на курсор мыши кнопки, за счет функции Elastic](https://habrastorage.org/getpro/habr/upload_files/7b3/93f/e75/7b393fe75f9d2c98a89c4ca70694764e.gif)
Еще пара Easing Functions
Изначально Роберт Пеннер придумал 30 функций, но позднее разработчикам стало очевидным, что не помешало бы еще что-то. Например, в некоторых ситуациях требуется замедление анимации посередине, а максимальная скорость на краях — так появилась OutIn (вместо привычной InOut):
float easeOutInQuart(float x)
{
if (x < 0.5f)
{
x -= 0.5f
return 0.5f - 8 * x*x*x*x;
}
else
{
x -= 0.5f;
return 0.5f + 8 * x*x*x*x;
}
}
В результате получаем анимацию, похожую на эффект замедления времени (как в «Матрице»):
![](https://habrastorage.org/getpro/habr/upload_files/f65/53d/8fe/f6553d8fe543303a9acd12a9f0e9d17c.gif)
А что, если у анимации нет конца?
Такое часто бывает, когда учитывается пользовательский ввод: в интерфейсах приложений или в играх. Например, у нас есть объект, который плавно и постоянно должен двигаться до курсора мыши:
![](https://habrastorage.org/getpro/habr/upload_files/da8/191/ce8/da8191ce843b76691651c2f810e08f52.gif)
![](https://habrastorage.org/getpro/habr/upload_files/d87/842/56b/d8784256b6395c3316cef74c97ac7037.gif)
![](https://habrastorage.org/getpro/habr/upload_files/2bd/63d/e6c/2bd63de6c371a012ec410b9d60955b06.gif)
Или скроллинг:
![](https://habrastorage.org/getpro/habr/upload_files/443/4db/339/4434db339f93f5f44771fc6a8c86dbfe.gif)
Плавный драг-н-дроп:
![](https://habrastorage.org/getpro/habr/upload_files/e01/1e8/9f5/e011e89f52fa97749d7db608b37fde13.gif)
Плавная слежка за объектом камерой:
![](https://habrastorage.org/getpro/habr/upload_files/867/80a/162/86780a1626330f611be7b74e6fd57e82.gif)
В таких случаях применяются анимации, основанные на физике (physics-based).
А где же взять физику?
![](https://habrastorage.org/getpro/habr/upload_files/899/56c/1fc/89956c1fc8da0dc0d7226f87ba660e83.png)
Сделать свою!
На самом деле это не так страшно как кажется, потому что кода придется написать ненамного больше, чем было в этом уроке. И как раз в следующем уроке мы и поговорим про создание контролируемых нелинейных анимаций через формулы из физики!
Комментарии (5)
panteleymonov
09.08.2022 18:56+2В интерактивных приложениях мы часто сталкиваемся с двумя видами кривых Безье: квадратичной и кубической.
Все бы ничего, но есть два дополнения:
квадратичная кривая Безье = lerp(lerp(p0,p1,t),lerp(p1,p2,t),t)
кубическая кривая Безье = lerp(lerp(lerp(p0,p1,t),lerp(p1,p2,t),t),lerp(lerp(p1,p2,t),lerp(p2,p3,t),t),t)
https://www.desmos.com/calculator/hfqqnck5g4?lang=ru
Ну и остальное = lerp(p0,p1,F(t))
lain8dono
09.08.2022 20:33+1Для тех, кого действительно интересуют кривые безье, сплайны и вот это всё: https://pomax.github.io/bezierinfo/ (это только базовая информация)
Кстати для сглаживания камеры и подобного есть кое-что лучше, чем бездушная кривая. Смотреть Game Programming Gems 4 Chapter 1.10
Refridgerator
10.08.2022 06:54их можно разделить на 3 категории: In (ускоряющие), Out (затухающие) и InOut (ускорение с затуханием в конце):
Математически имеет смысл делить по другому:
— монотонные,
— пульсирующие,
— с разрывами производной.
Соответственно и работать удобнее не с дискретным набором конкретных функций (поскольку придумать их можно миллион), а с одной, но параметризованной (например, прохождением через точки с заданными координатами).Пример монотонной параметризованной функции
domix32
А пиксельные анимации таки через PICO рисовали или на Unigine клон сделали?
Unigine Автор
Да, на PICO, брали отсюда:
https://www.lexaloffle.com/bbs/?tid=40577