Начиная с этой статьи, я хочу немного поговорить о физике.
Я не хочу писать сложные формулы и решать дифференциальные уравнения. Мне кажется, физику гораздо проще понять, если попытаться симулировать физические процессы, а не разбирать формулы.
Этим мы и займемся. Начнем с простой модели и будем её понемножку усложнять. Вас ждет код на TypeScript и много анимированных картинок :).
Цели
Прежде, чем приступать, мне бы хотелось сделать несколько оговорок.
- Мы не будем влезать в дебри теоретической физики.
- Мы не будем решать дифференциальные уравнения.
- Мы не будем рассматривать точные аналитические решения уравнений.
- Я не рассказываю о программировании.
- Я попытаюсь раскрыть саму суть физики, суть дифференциальных уравнений, как их следует "читать".
Цель этой статьи - создать физическую модель, приведенную ниже. Должен получиться простенький физический движок :).
Извинения
Заранее прошу прощения за то, что
- вставляю куски кода в виде картинок. Полный код доступен по ссылкам.
- разбираю код только нескольких самых основных функций.
- к сожалению, в TypeScript нет перегрузки операторов. Поэтому приходится писать формулы с помощью обычных Plus(), Mult() и т.п.
Правило производной
Чтобы описывать физические процессы, нам обязательно нужно вспомнить, что такое производная.
Определение производной следующее
Мы перепишем его в другом виде.
Полученную формулу я буду называть "правило производной".
Представьте, что у нас есть две независимые переменные f и f'. На каждом кадре значение переменной f мы должны увеличивать на величину f', помноженную на время, прошедшее между двумя кадрами. Чем меньше dt, тем точнее будет наша симуляция, тем ближе наше решение будет к точному аналитическому решению.
Давайте сразу перейдем к рассмотрению примеров.
Модель 1. Движение с постоянной скоростью.
Допустим, у нас есть две (векторные) переменные Postion и DPosition. Первая описывает положение шарика, вторая - его скорость.
В начале мы вызываем функцию Init(), в которой инициализируем эти две переменные.
Функция OnUpdate() вызывается на каждом кадре. В неё передается значение dt - это время, прошедшее с предыдущего кадра. В этой функции мы выполняем следующее.
Это наше правило производной (формула №1).
В результате у нас получается модель, где шарик движется с постоянной скоростью.
Модель 2. Движение с ускорением.
Предыдущая модель была слишком скучной. Я хочу добавить ускорение свободного падения, чтобы шарик двигался по баллистической траектории.
Для этого во второй модели мы создадим 3 переменные: Position, DPosition и DDPosition, которые мы проинициализируем в функции Init().
А теперь напишем функцию OnUpdate().
Как видите, наше "правило производной" применяется теперь дважды. Сначала к паре переменных Position и DPosition, а затем к паре DPosition и DDPosition.
Как видим, наш шарик теперь движется по параболе, что мы и ожидали.
Теория
Далее я хочу добавить в нашу модель силы. Для этого нам нужно вспомнить немного школьной физики.
Вспомним знаменитую формулу Ньютона.
Где F - это сила, m - масса тела, a - ускорение.
Формулу Ньютона следует читать, как "сила - это ускорение". Как только на тело воздействует сила, она придает этому телу ускорение. А масса - это коэффициент пропорциональности между ними.
Одна и та же сила легким телам будет придавать большее ускорение, а тяжелым - меньшее.
Сила притяжения тела к Земле описывается формулой
Если предположить, что расстояние от центра Земли до объекта почти не меняется, и объединить константы, то получится формула
где - g - ускорение свободного падения Земли, m - масса объекта
Эту формулу мы и будем использовать дальше.
Модель 3. Сила гравитации.
Давайте переделаем немного нашу модель.
Мы уберем переменную, отвечающую за ускорение, оставим только координату и скорость.
Ускорение будем вычислять на каждом кадре в функции OnUpdate().
Как видите, сначала мы вычисляем силу, как ускорение свободного падения помноженную на массу тела. Потом мы "конвертируем" силу в ускорение, деля её на массу.
Может показаться, что мы совершаем лишние действия. Сначала умножаем на массу, а затем обратно делим.
Так и есть. Но это более общая схема. Сначала нужно вычислить все силы, действующие на объект. Потом нужно "сконвертировать" силу в ускорение. А затем ускорение и скорость действуют по правилам производных.
То, что сила гравитации пропорциональна массе объекта и, как следствие, придает всем телам одинаковое ускорение, это фишка исключительно гравитации. Не следует это принимать, как общее правило.
Поведение шарика в модели 3 ни чем не отличается от модели 2. Мы просто чуть-чуть зарефакторили код, введя понятие силы.
Модель 4. Отскок от стен.
Далее я хочу добавить отскок шарика от пола и стен.
Мы оставим функцию инициализации без изменения.
А функцию OnUpdate() немного изменим.
Сначала мы вычисляем силу гравитации, которая действует на объект.
Затем мы проверяем высоту шарика. Если шарик касается пола, то мы прикладываем дополнительную силу, которая толкает шарик вверх.
Таким же образом мы добавляет силы, отталкивающие шарик от стен.
Дальше, все как и прежде. Конвертируем силу в ускорение. А ускорение и скорость действуют по "правилам производной".
По хорошему, сила упругости стен должна зависеть от глубины проникновения шарика. Но я для простоты сделал её постоянной.
Как видим, шарик теперь научился отскакивать от пола и от стен.
Мы могли бы симулировать отскок от пола более простым способом. А именно, когда шарик опустился слишком низко, просто инвертировать скорость по вертикали.
Но я специально избегаю подобных хаков. Мне хотелось показать суть работы физики. Ведь в реальном мире все именно так и происходит. Когда шарик приближается к стенке слишком близко, возникают отталкивающие силы, которые разгоняют шарик в противоположном направлении.
Модель 5. Много взаимодействующих объектов.
Полученных знаний уже достаточно, чтобы создать нашу финальную модель, в которой несколько шариков отскакивают от стен и друг от друга.
Но для этого придется написать гораздо больше кода.
Во-первых, нужно создать класс, описывающий один шарик.
Правило производной мы поместим в функцию OnUpdate() этого класса. Внутри этого же класса мы определили силу Force. Но прикладывать силы к шарику будем уже извне класса.
В функции Init() мы инициализируем список шариков.
Так выглядит функция OnUpdate().
Каждый кадр мы вычисляем все силы, действующие на шарики. Силы постепенно накапливаются в зависимости от того, с кем шарики контактируют.
Поэтому в начале мы обнуляем все силы.
Затем пробегаем по каждому шарику и добавляем силы гравитации и силы отталкивания от стенок, если шарик с ними контактирует.
Затем идет двойной цикл. В нем мы попарно сравниваем шарики каждый с каждым, чтобы вычислить силу взаимодействия между ними. Если шарики далеко друг от друга, то функция GetCollideForce() возвращает нулевую силу. Если шарики сблизились на достаточное расстояние, то возвращается сила отталкивания.
Обратите внимание на строчки
Это проявление еще одного закона Ньютона.
Если два тела взаимодействуют между собой, то на одно тело действует одна сила, а на второе - действует та же сила, но в противоположном направлении. Т.е. силы всегда прикладываются попарно! Но ускорения, которые тела приобретают, может быть разным в зависимости от их масс.
Например, когда на тело действует сила гравитации со стороны Земли, то точно такая же сила действует и на саму Землю. Но Земля слишком тяжелая, чтобы "почувствовать" эту силу. Поэтому мы ею обычно пренебрегаем.
Дополнительно приведу код функции GetCollideForce().
Сначала мы вычисляем вектор delta, соединяющий центры двух шариков.
Затем вычисляем его длину и, тем самым, расстояние между шариками.
Если шарики приблизились достаточно близко, то возвращаем вектор по направлению от первого шарика ко второму с длиной ballElasticityForce.
Иначе возвращаю нулевой вектор.
Заключение
Как видите, полученная модель ведет себя довольно правдоподобно. Хотя мы даже не рассмотрели вращение объектов, силы трения и прочее.
В следующих статьях мы продолжим рассмотрение разных моделей. А так же поговорим о том, как правильно "читать" дифференциальные уравнения.
Рекомендую читателю самостоятельно запрограммировать эту же модель на доступном ему языке программирования. Это очень увлекательно! Можно почувствовать себя немножко богом, создающим вселенную :). Можно создать планеты, вращающиеся вокруг солнца, или симулировать волны.
Мы еще коснемся планет и волн в следующих статьях.
продолжение следует...