Как работает Drag в физике Unity

в 17:57, , рубрики: drag, unity3d, физика

Осознавая что мои статьи похожи на восторг человека, впервые увидевшего какую-то (многим уже давно знакомую) вещь, все же решил написать очередную. Причина — заинтересовавший меня параметр drag у компонента Rigidbody. Объяснений и, тем более, формул — как он работает — руководства не дают. Поэтому разбираться придется самому.

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

При знакомстве с физикой в Unity первое, чему учат, это тому, что Rigidbody — воплощение физических свойств объекта. У него есть пара параметров, задающих его взаимодействие с окружающим миром. Собственно, наиболее значимый для динамических объектов: масса. С ней все интуитивно понятно: закон Ньютона и все такое… Приложили силу кратковременно — получили импульс и, соответственно, изменили скорость объекта. Прикладываем длительное время: получаем ускорение в зависимости от массы.

Кроме этого, у Rigidbody есть интересный параметр — drag. Выражаясь словами разработчиков: параметр drag задает drag объекта. На их языке drag — это замедление, торможение. Других объяснений — не дается. Говоря о физике и торможении, в голову приходит что-то из баллистики (аэродинамики, гидродинамики и пр.). Возможно, drag — это некий коэффициент, пропорциональный квадрату скорости. Это было бы разумно. Но.

Простой опыт показывает, что в Unity тела с большим значением drag не двигаются вообще. Гравитация на них не влияет, толкнуть их невозможно. Но при этом они стоят, то есть квадрат их скорости равен нулю. Значит тут все не так просто… или же, наоборот, не все так сложно. Физика в компьютерных играх должна просчитываться быстро, а значит возможны упрощения относительно реальной физической модели. Хотя реализовать игру, скажем, с релятивистскими эффектами было бы забавно…

Первый вопрос, которым я задался: хотя бы, масса в Unity ведет себя как масса? Эксперимент показал, что — да.

Проверка массы/гравитации

Для начала, в простой сцене я запустил шарик с массой 1 с высоты 5 метров на плоскость. Триггер на плоскости останавливал таймер, так что я мог наблюдать время падения шарика.

image

Расчетное время падения для него должно составлять: t = sqrt(2*h/g), где h — высота, g — ускорение свободного падения. В нашем случае (h=5м, g=9.81м/с^2) t ≈ 1.0096.
Запуск показал значение 1.02, что близко к нужному.

image

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

void FixedUpdate()
    {
        rb.AddForce(0, -9.81f, 0);
    }

Результат оказался тот же самый (t=1.02). Неплохо.

Для действующей силы были затем использованы несколько разных значения. И всегда изменение действующей силы (rb.AddForce(0, xxx, 0);) вызывало изменение времени «падения», совпадающее с расчетным. Так что модель взаимодействия сил и массы оказалась вполне знакомой, ньютоновской.

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

image

Для начала стоило проверить простое падение без замедления (drag=0). Код, двигающий второй шарик был довольно прост:


void FixedUpdate()
    {
        Vector3 newVelocity = rb.velocity + gForceVector * rb.mass * Time.deltaTime;
        rb.velocity = newVelocity; 
    }

rb — это компонент Rigidbody сферы. gForceVector — вектор гравитации (0, 9.81, 0).

image

Пока все совпадает.

Теперь, левому шарику увеличиваем drag (drag=2). Он стал падать медленнее:

image

Интересный факт. Увеличение параметра drag до больших значений (бо́льших или равных примерно 50) приводило к тому, что шарик вообще не двигался. Изменение массы объекта никак не влияло на этот факт, что привело к мысли, что параметр drag — кинематический, то есть он зависит (и влияет) на положение, скорость, ускорение и т.д., не принимая в расчет силы или импульс.

Что еще могло влиять на алгоритм, где фигурировал drag? Возможно, частота обновления физики. Хотя это означало бы не самый лучший ход со стороны разработчиков. Но чем черт не шутит. Поменяем время обновления физики до 0.005 секунд.

image

Зависимость все же имеется. Однако, похоже, это вычислительные ошибки. Дальнейшие увеличения частоты обновления физики существенно время падения не меняют. Что, кстати, означает, что где-то в коде разработчиков должно быть что-то вроде "...*Time.fixedDeltaTime".

Итак, что мы знаем про параметр drag? Он — кинематический (считается после действия всех сил). Умножается (хотя возможно и не он сам) на Time.fixedDeltaTime и уменьшает скорость. Самое простое, что можно было бы придумать с такими условиями, выглядит примерно так:

void FixedUpdate()
    {
        Vector3 newVelocity = rb.velocity + gForceVector * rb.mass * Time.deltaTime;
        newVelocity = newVelocity - newVelocity*myDrag*Time.deltaTime;
        rb.velocity = newVelocity; 
    }

Новое действие выглядит довольно примитивно: одно умножение и одно вычитание (с поправкой на то, что действия осуществляются с векторами). Однако, с чего-то нужно начать. Проверяем этот код, для удобства автоматически присваивая переменной myDrag (из скрипта второй сферы) значение параметра drag первой сферы.

drag=1
image

drag=3
image

drag=15
image

drag=50
image

Собственно, здесь оба шарика так и не начали движение. Мне просто надоело ждать.

Итак, результат получился для меня неожиданный. Примитивный алгоритм, который задумывался как начальная точка, от которой бы я дальше отталкивался — оказался конечной точкой. Похоже, именно так и считается замедление в Unity с использованием параметра drag. С другой стороны — проще, значит быстрее (в плане времени выполнения).

Дальнейшие эксперименты расхождений между моим скриптом и встроенной физикой почти не показали. Почти. Оставалась одна мелочь. При значении drag=100 мой второй, движимый скриптом шарик, величественно поплыл вверх.

drag=100
image

В общем-то, этого следовало ожидать, исходя из алгоритма. Однако, в Unity подобного не наблюдается. Следовательно, нам осталась одна простейшая модификация кода:

void FixedUpdate()
    {
        Vector3 newVelocity = rb.velocity + gForceVector * rb.mass * Time.deltaTime;
        newVelocity = newVelocity * Mathf.Clamp01(1f - myDrag*Time.deltaTime);
        rb.velocity = newVelocity; 
    }

Теперь, «отрицательное» движение исключено и мы имеем полноценную имитацию действия сил и торможения движка Unity.

Физический аналог непосредственно параметра drag подобрать проблематично. Можно лишь сказать, что он определяет нелинейное трение (или нелинейное сопротивление — кому как удобнее). При этом сам алгоритм прост и, вероятно, быстро выполняем. А потому, если от физической модели не требуется какой-то особой достоверности или большой предсказуемости — использование параметра drag не лишено смысла.

Автор: cbr2002hell

Источник

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


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