Осознавая что мои статьи похожи на восторг человека, впервые увидевшего какую-то (многим уже давно знакомую) вещь, все же решил написать очередную. Причина — заинтересовавший меня параметр 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 не лишено смысла.
Поделиться с друзьями
-->

Комментарии (14)


  1. GreatRash
    23.12.2016 10:12

    https://en.wikipedia.org/wiki/Drag_(physics)


    1. EndUser
      23.12.2016 11:17
      +1

      Это как дела обстоят в нашем мире.

      А в Unity3D этот драг имеет линейный характер от скорости. Припомнить такой закон я не смог. Во всяком случае из школьной программы — динамическое сопротивление в среде от квадрата скорости, сопротивление «по асфальту» не зависит от скорости.

      Недавно получил ответ от автора Tank Physics Kit, в котором буквально говорится:

      I think that PhysX is just a stuff for video games, not a genuine physics simulator.
      Even when we set the actual values into the physics components, they will not work realistically.


      Курение мануалов по Wheel на сайте nVidia\PhysX к позитивным результатам не привело.

      Посему я понял, что если динамика игры претендует слегка походить на наш мир, её надо писать самому. Что касается кинематики, я ещё не определился — я не в курсе верны ли в Unity3D вращательные движения.
      _______________________

      cbr2002hell огромное спасибо за раскрытую тему!


      1. cbr2002hell
        24.12.2016 19:17

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


        1. cbr2002hell
          24.12.2016 21:31

          Сам спросил — сам ответил. Момент инерции считается по коллайдеру, предполагая, что тело однородно по плотности. Проверил на примитивах: сферах, кубах, цилиндрах.

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

          AngularDrag используется идентично линейному:

          rb.angularVelocity = rb.angularVelocity * Mathf.Clamp01(1f - myDrag*Time.deltaTime);
          


          1. EndUser
            25.12.2016 08:13

            Я в древности на бумажке прикидывал. Выходило, что момент инерции описывается тензором
            Ixx Ixy Ixz
            Iyx Iyy Iyz
            Izx Izy Izz
            Однако тогда я заметил и то, что некоторые игроделы ограничивались только Ixx, Iyy, Izz — это компоненты, достаточные для вычисления вращения только по координатным осям, и, как я понимаю, не совсем недостаточны для случая произвольной оси.
            В документах Unity3D действительно мелькала фраза, что они инерцию считают по равномерному распределению массы в пределах 3Д модели (и суммируют присоединённые меши, если таковые есть), но генерируют ли они полный тензор для вращения по произвольной оси…
            Здесь разница в том, что в линейной кинематике a = F/m, это три скаляра (ну пусть даже два вектора и скаляр), и они не грузят процессор.
            Во вращательной кинематике ?=M/I, формула «такая же». Но вектор момента силы делится на тензор инерции, что уже не так тривиально. А уж вычисление самого тензора потребует интегрирование по объёму 3D модели. У меня подозрение, что мало кто из игроделов серьёзно будет озадачиваться таким вопросом. Вероятно тут силён соблазн сурово сэкономить (=схалтурить) до уровня Ixx, Iyy, Izz.
            Но как смоделировать контрольную ситуацию для сверки Unity3D и правильного вычисления я не сообразил, потому, что интегрировать по объёму мне трудно.

            Опять же спасибо за выявление формулы «трения» вращения! :-)


            1. cbr2002hell
              26.12.2016 10:01

              Как выяснилось, типовая физика вращения в играх просчитывается в локальных координатах тела. А потому они хранят лишь одну (предпросчитанную) матрицу Ib, а для произвольного положения момент инерции находят как R*Ib*RT (ну, если точнее — они находят обратную).
              Но, как вы заметили, проверить, что в Unity реализуется именно эта модель — проблематично. Остается принять это как наиболее правдоподобное положение.


              1. EndUser
                27.12.2016 08:58

                Разница между I и I' только в ориентации тела, и есть операции, трансформирующие тензор согласно этому. Но сущности тензор не поменяет.

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

                Тут и есть соблазн «библиотекаря», разложить «пользовательское» вращение по произвольной локальной оси на последовательность вращений по удобным «библиотекарю» Ox', Oy', Oz', и использовать только три ячейки, Ix'x', Iy'y', Iz'z' из 9-значного тензора. Этим создаётся видимость вращения по произвольной оси. Но, предполагаю, может возникнуть погрешность.

                Представьте себе, что вы считаете длину гипотенузы через сумму длины и высоты ступенек. Как бы вы не уменьшали размер ступеньки, для равнобедренного прямоугольного треугольника вы получите гипотенузу, равную 1+1, а не корректное v2.

                То есть суть эксперимента истины я представляю так: возьмём пусть даже простую известную фигуру параллелепипед, высчитаем для него верный тензор со всеми 9 ячейками через интеграл момента инерции всех его атомов; затем начнём вращать его процедурно ?d=Md/I' по какой либо его диагонали, и рядом вращать такой же с использованием собственных технологий «библиотекаря». Если пойдёт погрешность, то можно через известные для параллелепипеда Ixx, Iyy, Izz попытаться выяснить, при как именно считает «библиотекарь».
                Или с чистой совестью успокоиться на том, что «2=v2 точно» ;-)
                Лично у меня цена этого вопроса (ярость выкурить том кинематики и первый курс матана) покуда не достигла необходимой для проверки.


                1. cbr2002hell
                  29.12.2016 16:12

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

                  AngularVelocity = AngularVelocity + I_inv*torque*Time.deltaTime - I_inv*Vector3.Cross(AngularVelocity,(I*AngularVelocity))*Time.deltaTime;
                  

                  здесь I и I_inv — заранее просчитанные прямая и обратная матрица момента инерции; torque — приведенный к локальной системе суммарный момент.
                  Все, что теперь нужно — получить новый кватернион ориентации:
                  deltaOrientation = 0.5f*(Orientation * AngularVelocity)*Time.deltaTime;
                  Orientation = Orientation + deltaOrientation;
                  NormalizeQuaternion(Orientation);
                  
                  

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


  1. V0odo0
    23.12.2016 11:32

    Простите, а зачем вы умножаете на Time.deltaTime в методе FixedUpdate? Ведь в этом случае значения будут высчитываться в зависимости от частоты кадров, что не есть правильно. Может там должно быть фиксированное значение Time.fixedDeltaTime?


    1. cbr2002hell
      23.12.2016 12:15

      В общем-то, сами разработчики рекомендуют везде и всюду (что в Update(), что в FixedUpdate()) использовать на Time.deltaTime (например, тут https://docs.unity3d.com/ScriptReference/Time-fixedDeltaTime.html). Однако, я и правда где-то встречал мнение, что drag умножается именно на fixedDeltaTime. Единственное — я не помню, как это там обосновывалось.


      1. alex_zzzz
        24.12.2016 19:07

        В контексте выполнения FixedUpdate свойство Time.deltaTime возвращает значение равное Time.fixedDeltaTime, так что разницы нет. Использовать везде Time.deltaTime рекомендуется просто для однообразия.


        1. cbr2002hell
          24.12.2016 19:09

          В целом, меня эта разница не особо интересует. А вот буржуи на каком-то форуме обсуждали, и мелькало сообщение, что fixedDeltaTime возвращает предустановленную константу (ту, которую в настройках можно менять), а deltaTime — реальное значение времени. Лично у меня загрузить машину так, чтобы начала тормозить физика никогда не получалось, так что для меня эти переменные равнозначны.


    1. Iq51
      24.12.2016 19:07

      Из справки к Time.deltaTime: «When called from inside MonoBehaviour's FixedUpdate, returns the fixed framerate delta time»


      1. cbr2002hell
        24.12.2016 19:10

        И вдогонку из справки к Time.fixedDeltaTime: «For reading the delta time it is recommended to use Time.deltaTime instead because it automatically returns the right delta time if you are inside a FixedUpdate function or Update function.»