Об использовании и понимании функции Lerp в Unity3D

в 11:54, , рубрики: unity3d

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

Написание статьи связано с общением с такими же как я непрофессионалами и выявлением некоторых разногласий по поводу поведения популярного куска кода с использованием функции Lerp в разных туториалах.

Нас интересует функция Lerp в Unity3D. А конкретнее, метод Lerp класса Vector3D. Впрочем, все «лерпы» в Юнити работают более или менее идентично. Итак, классический образчик использования Lerp в Юнити, фигурирующий во многих туториалах:

...
Vector3 destinationPoint;
float smoothing;
...
void Update()
{
//Плавное "скольжение" из одной точки в другую
transform.position = Vector3.Lerp (transform.position, destinationPoint, smoothing * Time.deltaTime);
}

По идее, в данном коде объект, к которому относится скрипт, вместо моментального переноса в точку destinationPoint плавно туда передвигается. Код — по сути — простой. Однако, в его простоте есть определенные нюансы.

Первый момент возник, когда один человек, кинув взгляд, сказал, что этот код линейно интерполирует положение объекта из текущего в destinationPoint.

Собственно, функция Lerp — это и правда попросту линейная интерполяция. Но это сама функция. А вот действие кода вышеупомянутого метода Update уже не столь очевидно. При его применении видно, что объект сдвигается в нужном направлении, и по ходу дела замедляется. В случае же «истинной» линейной интерполяции объект должен начать движение, перемещаться с фиксированной скоростью, затем резко замереть. Именно так происходит, если обращаться к вариантам использования Lerp не из туториалов, а из официальной справки по скриптам (конкретно, по методу Lerp). А вот упомянутый код ведет себя не так.

Второй момент возник, когда другой человек сказал, что это вообще работать не должно. Функция Lerp должна принимать параметр (назовем его традиционно t), который меняется от 0 до 1. Соответственно, при нуле — положение соответствует начальной точке пути; при единице — конечной. Стало быть, когда параметр пробегает значения от 0 до 1 происходит перемещение от начальной точки к конечной. Но ведь в вышеуказанном коде параметр t практически не меняется! Переменная smoothing, задающая «гладкость» движения — фиксированна. deltaTime меняется в случайных пределах, но грубо-примерно находится на одном уровне; для постоянной частоты кадров она будет постоянна. Таким образом можно считать, что параметр t вообще не меняется, а значит и не будет меняться положение точки. И движения не будет.

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

Почему так происходит? Потому что положение начальной точки интерполяции меняется при каждом обновлении.

Примем для простоты, что частота кадров стабильная, и произведение параметра сглаживания на длину кадра равно 0.25 (одна четверть). Пусть нам надо пройти путь d от начальной точки до конечной:

image

В результате выполнения

Vector3.Lerp (transform.position, destinationPoint, 0.25);

получаем точку на расстоянии четверти от начала. Эта-то точка и становится новым положением объекта, и при следующем вызове метода Update() плясать мы будем уже от нее. Это к вопросу: почему это все же работает.

image

Новое расстояние d' теперь меньше (поскольку объект придвинулся). Стало быть, четверть от этого расстояния будет тоже меньше. В итоге, объект сдвинется еще ближе к точке назначения, но уже на меньшее расстояние.

image

При следующем обновлении — объект пройдет еще меньшее расстояние.

Это уже к вопросу: почему используем линейную интерполяцию, а получаем нелинейное движение. Чем ближе объект к точке назначения, тем меньшее расстояние ему до него остается, но тем и меньше шаг, который он сделает, подобно объекту из апорий Зенона.

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

Такое использование функции 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, которая, хоть и требует хранения дополнительной переменной, ведет себя более предсказуемо (позволяя, например, задать примерное время «прибытия» объекта на заданное место).

Всем спасибо за внимание.

Автор: cbr2002hell

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js