Vector3 lerp что это
Об использовании и понимании функции Lerp в Unity3D
Приветствую хабровчан. Решил написать статью об одной теме, которую недавно затронул в общении со знакомыми людьми. Тема касается не столько использования, сколько понимания типового использования этой функции. Собственно, я — не особо программист, и не разработчик, так что мой взгляд — взгляд, так сказать, обывателя-дилетанта.
Написание статьи связано с общением с такими же как я непрофессионалами и выявлением некоторых разногласий по поводу поведения популярного куска кода с использованием функции Lerp в разных туториалах.
Нас интересует функция Lerp в Unity3D. А конкретнее, метод Lerp класса Vector3D. Впрочем, все «лерпы» в Юнити работают более или менее идентично. Итак, классический образчик использования Lerp в Юнити, фигурирующий во многих туториалах:
По идее, в данном коде объект, к которому относится скрипт, вместо моментального переноса в точку destinationPoint плавно туда передвигается. Код — по сути — простой. Однако, в его простоте есть определенные нюансы.
Первый момент возник, когда один человек, кинув взгляд, сказал, что этот код линейно интерполирует положение объекта из текущего в destinationPoint.
Собственно, функция Lerp — это и правда попросту линейная интерполяция. Но это сама функция. А вот действие кода вышеупомянутого метода Update уже не столь очевидно. При его применении видно, что объект сдвигается в нужном направлении, и по ходу дела замедляется. В случае же «истинной» линейной интерполяции объект должен начать движение, перемещаться с фиксированной скоростью, затем резко замереть. Именно так происходит, если обращаться к вариантам использования Lerp не из туториалов, а из официальной справки по скриптам (конкретно, по методу Lerp). А вот упомянутый код ведет себя не так.
Второй момент возник, когда другой человек сказал, что это вообще работать не должно. Функция Lerp должна принимать параметр (назовем его традиционно t), который меняется от 0 до 1. Соответственно, при нуле — положение соответствует начальной точке пути; при единице — конечной. Стало быть, когда параметр пробегает значения от 0 до 1 происходит перемещение от начальной точки к конечной. Но ведь в вышеуказанном коде параметр t практически не меняется! Переменная smoothing, задающая «гладкость» движения — фиксированна. deltaTime меняется в случайных пределах, но грубо-примерно находится на одном уровне; для постоянной частоты кадров она будет постоянна. Таким образом можно считать, что параметр t вообще не меняется, а значит и не будет меняться положение точки. И движения не будет.
Опять же, реальность показывает, что это не так. Код работает, и объект перемещается куда нужно. Еще и с замедлением.
Почему так происходит? Потому что положение начальной точки интерполяции меняется при каждом обновлении.
Примем для простоты, что частота кадров стабильная, и произведение параметра сглаживания на длину кадра равно 0.25 (одна четверть). Пусть нам надо пройти путь d от начальной точки до конечной:
В результате выполнения
получаем точку на расстоянии четверти от начала. Эта-то точка и становится новым положением объекта, и при следующем вызове метода Update() плясать мы будем уже от нее. Это к вопросу: почему это все же работает.
Новое расстояние d’ теперь меньше (поскольку объект придвинулся). Стало быть, четверть от этого расстояния будет тоже меньше. В итоге, объект сдвинется еще ближе к точке назначения, но уже на меньшее расстояние.
При следующем обновлении — объект пройдет еще меньшее расстояние.
Это уже к вопросу: почему используем линейную интерполяцию, а получаем нелинейное движение. Чем ближе объект к точке назначения, тем меньшее расстояние ему до него остается, но тем и меньше шаг, который он сделает, подобно объекту из апорий Зенона.
Фактически, объект движется к точке назначения по обратноэкспоненциальной зависимости, постоянно приближаясь к нему, но никогда не достигая. Ну, это с математической точки зрения. С практической же — зависит от выбранных масштабов.
Такое использование функции Lerp определенно имеет право на жизнь, но понятности оно не дает. Люди, услышавшие словосочетание «линейная интерполяция» часто предполагают другое поведение. Кроме того, есть много интересных фишек, позволяющих превратить интерполяцию из линейной в другую. Основаны они, обычно, на изменении параметра t. Фокус в том, что при использовании указанного примера все эти наработки вести себя будут совсем не так, как ожидалось. Я полагаю, разработчики Unity3D сами-то понимают функционирование подобного кода, но не объясняют таких нюансов, видимо не желая нагружать лишней (по их мнению) информацией.
Привычно функция Lerp и подобные ей используется для получения ряда промежуточных значений (как правило от начального до конечного). В данном же коде она нужна для получения одной конкретной точки: при каждом вызове Update() она находит значение точки, делящей отрезок пути в заданном отношении.
Еще звучал интересный вопрос, который я не совсем понял: коль скоро значение параметра интерполяции не меняется, зачем там вообще deltaTime? Ну, собственно, хорошая практика кодинга в Unity предполагает независимость от частоты кадров. Разумеется, при нестабильности частоты кадров разницу в поведении кода, что с умножением на Time.deltaTime, что без оного — на глаз не заметно. Но факт есть факт.
Другой вопрос, который уже задал я сам себе: зачем тогда умножать на Time.deltaTime в методе FixedUpdate()? Ведь разработчики уверяют, что время, проходящее между вызовами этого метода строго фиксировано (почти… см. ниже). Однако, в туториалах код, подобный вышеупомянутому, попадается и в методе FixedUpdate() (например, тут).
Тут возможно несколько вариантов: возможно, ведущие этой обучалки, привыкшие к данному шаблону, попросту вбили его не задумываясь. Либо же гарантировали идентичность результатов выполнения кода на случай, если по какой-либо причине частота обновления физики (вызовов FixedUpdate()) будет изменена. А может просто решили не добавлять «магических констант», а заодно обеспечить определенную совместимость (и переносимость) кода между методами Update() и FixedUpdate(), поскольку в противном случае пришлось бы иметь отдельные smoothing для первого и для второго метода.
Вообще, с этими временами обновления тоже не все гладко. Для FixedUpdate() заведена своя переменная fixedDeltaTime, которая, судя по названию, должна давать время между его вызовами… Но нет же, сами же разработчики рекомендуют и в FixedUpdate() и в Update() использовать deltaTime, поскольку частота вызовов FixedUpdate() фиксированная-фиксированная, да не очень.
Так или иначе, итог.
Функция Lerp — действительно функция линейной интерполяции. Однако, популярный шаблон ее использование вовсе не линейно интерполирует перемещение и вращение объекта. Код отличается определенной краткостью, хотя и вызывает затруднения при попытках применения наработанных методик изменения поведения линейной интерполяции. При этом, действие этого кода, насколько мне известно, нигде в обучалках не объясняется, оставляя многих в заблуждении относительно того, как он работает.
Вообще, я предпочитаю функцию SmoothDamp, которая, хоть и требует хранения дополнительной переменной, ведет себя более предсказуемо (позволяя, например, задать примерное время «прибытия» объекта на заданное место).
Русские Блоги
Анализ линейной интерполяции функции Lerp () в Unity3D
В unity3D, функция линейной интерполяции Lerp () часто используется для интерполяции между двумя, которые могут быть между двумя материалами, между двумя векторами, между двумя числами с плавающей запятой, между двумя цветами, Прототип функции выглядит следующим образом:
function Lerp(start : Material, end : Material, t : float) : void
Интерполировать между двумя материалами
static functionLerp (from : Vector2, to : Vector2, t : float) : Vector2
Линейная интерполяция между двумя векторами. Интерполировать от формы к в соответствии с числом т.
t находится между 0 и 1. Когда t = 0, вернитесь из. Когда t = 1, вернитесь к. Когда t = 0,5, верните среднее значение от и до.
static functionLerp (from : Vector3, to :Vector3, t : float) :Vector3
Линейная интерполяция между двумя векторами. Интерполировать от в до в соответствии с числом т.
static functionLerp (from : Vector4, to : Vector4, t : float) : Vector4
5. Mathf. Lerp интерполяция
static functionLerp (from : float, to : float, t : float) : float
На основе числа с плавающей точкой t возвращает интерполяцию между a и b, t ограничено 0
1. Когда t = 0 возвращается из, когда t = 1 возвращается в. Когда t = 0,5, возвращается среднее значение от и до.
6. Color.Lerp интерполяция
static functionLerp (a : Color, b : Color, t : float) : Color
Интерполируйте между цветами a и b через t.
static functionLerp (from : float, to : float, t : float) : float
На основе числа с плавающей точкой t возвращает интерполяцию между a и b, t ограничено 0
1. Когда t = 0 возвращается из, когда t = 1 возвращается в. Когда t = 0,5, возвращается среднее значение от и до.
Сначала давайте проведем эксперимент, запустим Unity3D, создадим файл сценария и введите в его Start () следующее:
void Start () <
print(Mathf.Lerp(0.0f, 100.0f,0.0f).ToString());
print(Mathf.Lerp(0.0f, 100.0f,0.1f).ToString());
print(Mathf.Lerp(0.0f, 100.0f,0.2f).ToString());
print(Mathf.Lerp(0.0f, 100.0f,0.3f).ToString());
print(Mathf.Lerp(0.0f, 100.0f,0.4f).ToString());
print(Mathf.Lerp(0.0f, 100.0f,0.5f).ToString());
print(Mathf.Lerp(0.0f, 100.0f,0.6f).ToString());
print(Mathf.Lerp(0.0f, 100.0f,0.7f).ToString());
print(Mathf.Lerp(0.0f, 100.0f,0.8f).ToString());
print(Mathf.Lerp(0.0f, 100.0f,0.9f).ToString());
print(Mathf.Lerp(0.0f, 100.0f,1.0f).ToString());
>
Запустите Unity, консоль распечатает:
Этот эксперимент интерполирует между 0 и 100. Какое значение для вставки зависит от третьего параметра. Как видно из результатов печати, третий параметр является масштабным коэффициентом. Когда он равен 0,1, это означает, что длина от 0 до 100 составляет десятые доли. Один по той же причине 0,2 означает две десятых и т. Д. С этой точки зрения, интерполяция, которую мы буквально поняли в начале, состоит в том, чтобы вставить значение, которое можно понять таким образом.
Если мы изменим первый параметр в функции интерполяции в приведенном выше сценарии на 100.0f, второй параметр на 110.0f, а третий параметр останется неизменным, что вы думаете о результате операции? Не думайте, что это 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, фактический результат равен 100, 101, 102, 103, 104, 105, 106…. Поскольку интерполяция Интерполяция значения между исходными двумя числами означает, что эта функция сначала вычисляет чистое приращение на основе отношения, заданного третьим параметром, добавляет начальное число и, наконец, вычисляет интерполированное значение.
В разработке игр для Unity3D наиболее широко используется векторная интерполяция Vector3.Lerp. Ниже мы используем эту интерполяцию, чтобы угадать ее внутренний механизм реализации и некоторые приложения.
Пожалуйста, см. Рисунок ниже для значения координаты Y точки C:
EO / AO = DF / AF = CB / AC = 1-0,4 = 0,6, тогда значение координаты Y точки C EO = 0,6 * AO = 0,6 * 10 = 6.
Ниже мы используем интерполяционную функцию Vector3.Lerp в Unity3D: статическая функция Lerp (от: Vector3, до: Vector3,
t: float): Vector3 для расчета вычисленной выше интерполяции.
Мы переписали функцию Start () в предыдущем скрипте следующим образом:
Это согласуется с нашими результатами расчетов.
Для приведенного выше расчета для простоты две точки A и B сделаны специальными, что снижает сложность расчета. Для обычных A и B, как показано на следующем рисунке:
Функция Lerp больше используется в процессе разработки игры.В файле справки Unity мы перечислили два примера приложений Vector3.Lerp: один из них заключается в том, что позиция анимации перемещается от start.position к end.position за 1 секунду. конец:
// Следуем за целевым объектом как пружина
Давайте посмотрим на другой пример приложения.
Сначала установите пустой объект в точке поворота в соответствии с изгибом и волнистостью дороги. Значение позиции этого пустого объекта, то есть пространственные координаты, согласуются с дорогой здесь. Мы называем точку, где эти пустые объекты находятся, точкой поворота дороги. Эти точки связаны. Полилиния, состоящая из отрезков, прикреплена к поверхности дороги и является приблизительным путем этой дороги. Чем точнее эти точки, тем выше сходство между этой дорогой и дорогой.
Теперь мы используем этот путь, чтобы заменить эту дорогу, и поместим случайно сгенерированную баррикаду на этот путь, то есть на дорогу.
Предположим, что мы хотим генерировать контрольно-пропускной пункт каждые 100 метров до 200 метров, используйте переменную z + = Random.Range (100, 200), чтобы записать значение координаты Z контрольно-пропускного пункта (так как заключенные обычно бегут вдоль оси Z ) Затем в соответствии с этим значением Z-координаты определите, какие две точки в поворотной точке, между которыми задано значение координаты, и интерполируйте между этими двумя точками после нахождения масштабный коэффициент его интерполяции (функция Lerp () 3 параметра) можно рассчитать из известных значений Z-координат трех точек двух точек поворота и этой точки интерполяции, поэтому Vector3.Lerp
(от: Vector3, до: Vector3, t: float) Три значения параметров в функции известны, она может вычислить пространственные координаты этой точки интерполяции, в соответствии с предыдущим дизайном, эти две точки поворота Сегмент линии между ними помещается на дороге, тогда координата этой интерполяции находится на дороге, расположенная в соответствии с этой интерполяцией баррикада не будет отклоняться от дороги и будет поворачивать влево и вправо, когда дорога поворачивает влево, вправо Поверните направо, в гору и в гору, снова и снова в гору.
Конкретный процесс проектирования заключается в следующем:
Импортируйте модель дороги, предполагая, что она называется forest_1. Когда модель спроектирована, определяется, что ее длина составляет 3000, а начало координат находится на ее терминале. После импорта мы помещаем его в сцену вдоль положительного направления оси Z, так что значения X и Y его Transorm.Position равны 0. Мы можем импортировать многосегментные модели дорог одного типа и объединять их в длинные лесные дороги, контролируя их значения Z.
Таким образом, мы разделили извилистую дорогу на прямые участки и записали значения координат характерных точек на обоих концах каждого участка. С этими характерными точками есть также маршрут, близкий к дороге. Это метод превращения кривой в прямую, превращение изогнутой и волнистой дороги в отрезок, близкий к ней. Чем больше таких точек, тем выше степень сходства.
Создайте скрипт-компонент waypionts.cs на waypionts:
Интерполяция — мать анимации — Твинеры в Unity
Но есть и у этого инструмента слабое место. Все анимации жестко ограничены, они представляют собой заранее описанный сценарий, который просто воспроизводится на иерархии объектов. Впрочем, это не совсем так. Как уже было отмечено выше, аниматор это чрезвычайно сложный инструмент, во всех его UI можно найти множество кнопочек и настроек, каждая из которых, как ни удивительно, выполняет какую-то функцию. К примеру, Avatar Mask позволяет сделать так, что некоторый слой анимации будет управлять отдельными частями тела, и персонаж будет махать рукой сидя на лошади, хотя отдельной такой анимации на Mixamo не нашлось =( Аниматор просто отыгрывает 2 анимации параллельно на тех костях, которые были заданы масками.
Хорошо, с третьей попытки, постараемся все таки сформулировать мысль таким образом, чтобы не скатиться вновь в дифирамбы очередной функции аниматора. Прокрутив в голове конец первого абзаца как дисклеймер, начнем.
Допустим, нам нужна анимация, которая будет активно взаимодействовать с окружением. Например, при контакте с персонажем монетка должна подпрыгнуть, сверкнуть, и улететь в карман нашего героя. То есть, сценарий нашей анимации зависит от контекста своего выполнения. Или нам просто не нужен аниматор на конкретном объекте. Например, анимируя UI нужно очень постараться, чтобы аниматор не заставлял Canvas перерисовываться на каждом кадре. А также, аниматор ничем не сможет помочь, если мы работаем с несовместимыми интерфейсами. Например, если мы захотим сделать анимацию накопления при подсчете очков или двигать вершины меша(Может быть, даже взаимодействуя с окружением! Например, плавно помять корпус авто в точке удара). Здесь нам поможет только описание логики анимации с помощью кода. Наконец-то мы подобрались к основной теме данной статьи.
Если вы загуглите слово «Твинер«, без указания контекста найти что-то может оказаться не так то просто, результат потонет в более релевантных синонимах. Правильным запросом будет Inbetweening, сокращенно Tweening. Полная версия термина звучит уже более осмысленно и лучше говорит о сути происходящего.
Англоязычная википедия отсылает нас к старой доброй «аналоговой» анимации, когда промежуточные кадры рисовались на бумаге путем наложения соседних кадров на световом столе и рисования кадра «посередине «.
Твинер не справа и не слева. Он посередине
Думаю, ни для кого не секрет, что анимации хранятся в памяти не покадрово, в виде бесконечно плотной последовательности значений, для этого используются системы функций, определяющих значение параметра в зависимости от времени. Для гибкости настройки, за очень редкими исключениями, анимации не хранятся в виде чистых функций вроде синусоиды или многочлена. Разве что, в отдельных случаях можно столкнуться с кривыми Безье. Но чаще используются B-Сплайны. Кривые, построенные по опорным точкам, изгиб каждого фрагмента которых можно изменить меняя значение промежуточных точек. Таким образом можно двигать отдельные точки, и добиваться желаемого результата, не меняя общую картину.
вместо уверенного роста, функция изменила свое значение на интервале, чтобы соответствовать касательной, которая проходит через пару промежуточных точек
Здесь мы видим, что DOTween позволяет вызовом одной функции запустить анимацию того или иного параметра стандартных Unity-компонентов. Очень просто, удобно и красиво.
А вот более эффективный способ его использования, что отмечают и сами разработчики DOTween в документации. Здесь мы можем обратиться к полям любого типа и описать функцию, по которой должно изменяться их значение с течением времени.
Но на мой взгляд, единственный необходимый и самый полезный способ работы с DOTween выглядит вот так:
Это максимально абстрактное описание твинера, позволяющее наглядно и эффективно описать любую задачу и, как самые внимательные уже заметили, может существенно сократить сигнатуру до DOTween.To(duration, MyMethod) вызываемой функции и улучшить читаемость кода. Для большей понятности посмотрим, что может находиться MyMethod:
Проще говоря, вместо того, чтобы привязываться к конкретным типам, параметрам и длительности анимации, мы просто вызываем функцию, линейно прогоняя значение ее аргумента от 0 до 1. Внутри нее мы можем использовать функции плавности, чтобы некоторые из параметров менялись нелинейно, создавая более динамичную картинку. Например, label из примера пролетит увеличиваясь от нижней границы экрана и остановится в центре с небольшим заносом, цвет будет меняться линейно, а накопление счетчика будет происходить таким образом, чтобы последние цифры тикали дольше, создавая приятный визуальный эффект. Конечно, 3 действия из 4 можно было сделать не менее красиво, а вот чего-то более сложного код уже не будет таким лаконичным. Таким образом, мы создаем очень простую и понятную абстракцию, которую легко читать и комфортно использовать вне зависимости от контекста.
Как уже сказал, в Dotween есть возможность объявлять цепочки анимаций. После завершения предыдущего твина будет выполняться следующий или разовый Callback, позволяющий сообщить о завершении анимации и отобразить кнопку «Далее»
Здесь проблема DOTween прежняя. Как только мы выходим из зоны комфорта, начинаются бесконечные страдания и нестерпимая боль. Как будет выглядеть подобная цепочка?
А еще где-то должна быть реализация. Я предпочитаю использовать лямбды в описании твинера.
В целом, вроде бы не так и страшно, но с аргументами после лямбды крайне неудобно работать. Несколько хелперов позволяют найти золотую середину и получить удобный, производительный и гибкий твинер, совместимый с любыми видами объектов. Единственная оговорка в том, что твинер без использования лямбда-функций можно реализовать куда эффективнее, храня в отдельных структурах лишь ссылку на изменяемое значение и параметры анимации вместо лямбд, захватывающих контекст. Но мы теряем всю гибкость, так что едва ли игра стоит свеч.
К слову, о захвате контекста. Это больное место всех известных мне твинеров, включая и DOTween. Смена сцены в процессе анимации или уничтожение анимируемых объектов ломает его работу. Issue был открыт в марте 2016, и судя по комментариям, проблема есть по сей день. Вероятно, предполагается, что это настолько очевидно, что здесь даже не нужна защита от дурака. В конце концов, всегда можно подписаться на смену сцены и прерывать их исполнение. Или проверять существование объекта в процессе выполнения, потеряв львиную долю производительности и рискуя создать утечку памяти, захватив в зацикленном твинере ссылку на объект, но не затрагивая при этом выгруженный компонент, откуда мы эту ссылку получили. В этой проблеме кроется одна из главных причин, по которым я использую собственный твинер, и единственная, по которой я взялся его писать.
Проблема захвата контекста решается следующим образом: По умолчанию, анимация создается в основной категории, которая очищается при смене сцены. Но если мы анимируем объект, помеченный как DontDestroyOnLoad, при создании твинера мы явно это указываем. В таком случае он попадет во вторую группу и не будет выгружен при смене сцены. Интерфейс заточен под использование линейной интерполяции, и поэтому выглядит следующим образом:
Стандартная длительность анимации в 0.3 секунды была подсмотрена в гайдлайнах Material Design и очень хорошо зарекомендовала себя на практике. Это та длительность, при которой анимация заметна, ощущается ее плавность и эффект от Easing-функции, но не заставляет пользователя ждать. Обычно я указываю бóльшую длительность для каких-то особых анимаций, которые сопровождают значимое действие, но для 90% анимаций это аргумент, который удобнее будет скрыть.
Ниже фрагмент кода, позволяющий заанимировать экран победы с показом результата:
Эта статья задумывалась ни в коем случае не как сравнение различных реализаций или реклама своей(Которая, хоть и служит мне верой и правдой не первый год, никогда не претендовала на богатый функционал или хорошую производительность. Это наивная реализация очевидного алгоритма). В первую очередь, я хотел поднять проблему излишнего перехода от общего к частному в твинерах. Инструменты, которые должны обеспечивать эффективный и плавный переход в пределах интервала, всего лишь прогонять третий аргумент в функции Lerp, соревнуются в том, как много сахара они смогут насыпать в ваши проекты. А изначально необходимую функциональность приходится достигать путем двойной конвертации в виде аргументов функции DOTween.To. Спрятав Lerp в черный ящик они, пусть и упростили твинеры для пользования детьми, превратили их в бесполезные игрушки. Впрочем, как мы убедились выше, если очень постараться, DOTween можно использовать по назначению. И мыши плакали, кололись, но продолжали есть кактус.
Разумеется, было бы глупо исключать вероятность, что автор не прав, ожидания к твинерам завышены и не в ту степь, поэтому прикрепляю 2 опроса и буду рад вашим историям в комментарий: Что самое необычное вы анимировали с помощью твинера? Свой самый необычный кейс я упомянул в статье: плавная деформация корпуса авто в области коллизии путем перемещения вершин.