
Абсолютно упругий удар — это модель соударения, при которой полная кинетическая энергия системы сохраняется. В классической механике при этом пренебрегают деформациями тел. Соответственно, считается, что энергия на деформации не теряется, а взаимодействие распространяется по всему телу мгновенно. Хорошим приближением к модели абсолютно упругого удара является столкновение бильярдных шаров или упругих мячиков.
В статье мы будем рассматривать моделирование упругих столкновений на примере атомов одноатомного газа в двумерном пространстве.
Реализация
Я сначала расчет столкновений написал сам, потом спросил Claude, он мне показал более элегантную версию. Привожу его версию с небольшими корректировками.
Код на Javascript.
/** * @param {Atom} atom1 * @param {Atom} atom2 */ processCollision(atom1, atom2) { // vector from atom1 to atom2 const dx = atom2.pos.x - atom1.pos.x const dy = atom2.pos.y - atom1.pos.y const distance = Math.sqrt(dx*dx + dy*dy) const minNoIntersectionDistance = atom1.radius + atom2.radius if (distance >= minNoIntersectionDistance) return // cos and sin of vector angle const cosA = dx / distance const sinA = dy / distance // relative velocity const dvx = atom1.velocity.x - atom2.velocity.x const dvy = atom1.velocity.y - atom2.velocity.y // relative velocity to normal const dvn = dvx * cosA + dvy * sinA // if atoms go to opposite directions, there is no collision // NOTE: for negative time the equality sign should be opposite if (dvn < 0) return // impulse of elastic collision const impulseCoef = (2 * dvn) / (atom1.mass + atom2.mass) // update velocities const dvx1 = -impulseCoef * atom2.mass * cosA const dvy1 = -impulseCoef * atom2.mass * sinA const dvx2 = impulseCoef * atom1.mass * cosA const dvy2 = impulseCoef * atom1.mass * sinA atom1.velocity.add(dvx1, dvy1) atom2.velocity.add(dvx2, dvy2) }
Классы
class Point { constructor(x, y) { this.x = x this.y = y } add(x, y) { this.x += x this.y += y } } class Vector { constructor(x, y) { this.x = 0 this.y = 0 this.add(x, y) } add(x, y) { this.x += x this.y += y this.module = Math.sqrt(this.x * this.x + this.y * this.y) } cos() { return this.module < 0.0000001 ? 1 : this.x / this.module } sin() { return this.module < 0.0000001 ? 0 : this.y / this.module } } class Atom { /** * @param {Point} position * @param {number} radius * @param {number} mass * @param {Vector} velocity */ constructor(position, radius, mass, velocity) { this.pos = position this.radius = radius this.mass = mass this.velocity = velocity } }
Вызывается примерно так:
calcStep(dt) { for (let atom of this.atomList) { this.moveAtom(atom, dt) } for (let atom of this.atomList) { this.processCollisions(atom) } } /** * @param {Atom} atom * @param {number} dt */ moveAtom(atom, dt) { ... atom.pos.add(atom.velocity.x * dt, atom.velocity.y * dt) ... } processCollisions(atom) { const possibleCollisions = this.getPossibleCollisions(atom) for (let neighborAtom of possibleCollisions) { this.processCollision(atom, neighborAtom) } }
Что здесь происходит:
Получаем вектор направления из первого объекта на второй, это ось столкновения.
Получаем его длину, проверяем, есть ли пересечение объектов.
Находим вектор скорости первого объекта относительно второго.
Проецируем его на ось столкновения.
Если его длина отрицательная, значит атомы разлетаются, столкновение рассчитывать не надо.
Мы нашли проекцию скорости вдоль линии столкновения, делаем расчет по формуле упругого столкновения для одной оси с учетом масс.
Здесь все компоненты обрабатываются в декартовых координатах, то есть раскладываются на компоненты в виде проекций на оси координат. Можно считать в полярных координатах “длина, направление”, но проще вычисления не будут. Там все равно нужно учитывать только проекцию скорости на ось столкновения, а проекцию в перпендикулярном направлении оставлять как есть, и потом по ним рассчитывать финальную длину и направление векторов.
Схема:

Примечания
Ось столкновения называется нормаль, потому что она идет перпендикулярно линии касания поверхностей.
Нет особых причин, почему относительная скорость вычисляется именно как скорость первого относительно второго, наоборот тоже можно, поменяются только знаки в нескольких местах. Но я бы сказал, на рисунке так получается нагляднее, когда относительная скорость указана для первого атома.
Если просто напрямую считать финальные скорости по формуле из Википедии, то может быть так, что атомы будут каждый раз менять направление скорости на противоположное, потому что они не успевают разлететься и все еще пересекаются, хотя уже разлетаются. Поэтому нужно специально проверять разлет.
Two-dimensional collision
Dot product
В коде можно использовать отрицательное время, все объекты будут правильно двигаться в обратном направлении, но в проверке на разлет if (dvn < 0) return нужно поменять знак сравнения на обратный (“больше нуля”), иначе группы атомов будут собираться вокруг некоторого общего центра.
В этом коде столкновения рассчитываются попарно сразу для обоих атомов. Это значит, что если атом сталкивается сразу с 2 другими, при расчете второго столкновения будет использована уже новая скорость после первого.
Чтобы это исправить, можно добавить в атомы специальное свойство dv, которое будет хранить изменение скорости, и переносить его в вектор скорости только после обработки всех столкновений.
Но это все равно не даст правильную обработку. Например, когда один атом влетает сразу в 3 других атома, каждое столкновение будет считаться независимо, и импульс от атома 1 учтется полностью 3 раза, а должен распределяться на 3 атома. Claude и Google AI мне подсказали, что там нужно решать сложную систему уравнений.
Здесь можно почитать подробнее про столкновения нескольких объектов.
Интересно, как столкновения работают во Вселенной на низком уровне? Думаю, попарно, так как это значительно проще, просто на макроскопическом уровне различия незаметны.
Если квант времени будет слишком большой, то объекты могут пролетать сквозь друг друга. Поэтому квант времени нужно делать поменьше.
Теоретически можно вместо этого проверять столкновения не только в финальной точке, но и во всех точках пространства до нее. Это означает введение кванта пространства.
У этого есть интересное следствие. Чем больше скорость объекта, тем больше квантов пространства он проходит за квант времени. Чем больше квантов пространства он проходит, тем больше нам нужно проверить столкновений, и тем меньше у нас остается части от этого кванта времени на обработку других взаимодействий. То есть все процессы объекта начинают идти медленнее, собственное время объекта замедляется. Как в теории относительности.
Результат
Посмотреть результат и полный код можно тут.
GIF 8Mb

Гравитация
Можно добавить гравитацию. Просто добавляем нужную величину к вертикальной компоненте скорости всех атомов - ускорение, умноженное на время кадра. Для правильной работы гравитации dt всегда должно быть одинаковое, поэтому в обработчике requestAnimationFrame нужно делить прошедшее время на равные промежутки например по 10 мс, вызывать для каждого промежутка расчет шага, и переносить остаток на следующий кадр.
calcStep(dt) { // гравитация for (let atom of this.atomList) { atom.velocity.add(0, -this.gravity * dt) } for (let atom of this.atomList) { this.moveAtom(atom, dt) } for (let atom of this.atomList) { this.processCollisions(atom) } }
GIF 1Mb

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

wataru
28.06.2026 18:12А почему при столкновении нескольких объектов нельзя их обрабатывать попарно? Просто считать, что одна пара столкнулась на епсилон микросекунд раньше других? Ведь в пределе должно получиться тоже самое.
Надо только не двигать атомы, пока все столкновения не разрешатся.

michael_v89 Автор
28.06.2026 18:12С приведенным кодом оно так и получается. Просто шарики двигаются не так, как в бильярде, что может выглядеть неожиданно. Если один ударяет сразу 2, они получают разные импульсы. Примерно так.

wataru
28.06.2026 18:12Странно, я думал, что там в итоге должны те же скорости получатся. Но оказывается нет.
И вообще задача одновременного столкновения нескольких тел неразрешима. Там надо найти N импульсов, а у нас всего 2 уравнения: сохранение энергии и импульса.
И проблема в том, что при попарных столкновениях мы считаем что импульс сохраняется в паре, но при троичном столкновении импульсом обмениваются все три тела сразу, поэтому применять формулы для двух тел нельзя.

michael_v89 Автор
28.06.2026 18:12Если рассчитывать столкновения по очереди, то основной импульс двигающегося объекта уйдет в первое столкновение с неподвижным. Поэтому получаются разные скорости.
При попарных столкновениях общий импульс сохраняется правильно для любого количества тел, просто скорости распределяются не так, как мы ожидаем. Поэтому я предполагаю, что равномерное распределение импульса на 2 объекта, например для реальных шаров в бильярде, это просто следствие многократного столкновения множества частиц, составляющих шары. То есть некорректность не в количестве тел, а в том, что мы расчет для множества столкновений заменяем расчетом для одного.

jojozuka
28.06.2026 18:12Есть же физические движки (все перечислять не буду, например, pybullet), зачем изобретать велосипед?

michael_v89 Автор
28.06.2026 18:12Во-первых, для интереса.
Во-вторых, попробуйте сделать на них именно такую страницу для браузера. Ну или хотя бы исполняемый файл для desktop.
jojozuka
28.06.2026 18:12я на них много чего сделал, но конкретно такую страницу мне не надо

michael_v89 Автор
28.06.2026 18:12Ну так вопрос-то вы задали мне) Вот и ответ, на них такую страницу сделать нельзя, или как минимум это будет сильно сложнее.
kovserg
А ещё можно добавть потенциал Ван-дер-Ваальса и наблюдать фазовые переходы и формирование кристалов.
michael_v89 Автор
Можно, это будет аналог гравитации, только направление в сторону другого атома. Но тут есть такой нюанс, что на границе возникает потеря импульса. То есть при переходе из точки где потенциал 0 в точку где уже не 0 скорость меняется на одну величину, а в обратную сторону немного на другую. Если рассмотреть 2 атома, которые постоянно притягиваются по горизонтали и разлетаются после столкновения туда-сюда, то постепенно амплитуда уменьшается. Уменьшение кванта времени помогает, но не убирает проблему полностью. У меня не получилось найти способ считать точно. Наверно тут тоже надо вводить квантование каким-то способом.
DigLik_228653
Для сохранения энергии в закрытой системе, нужно использовать симплектически методы, например: полуявный метод Эйлера, также хорошо себя показывает метод Верле
michael_v89 Автор
Насколько я понял, в методе Эйлера сначала рассчитывается новая скорость. Я так и делал. Может где-то расчеты были не совсем правильные. Там еще дело в том, что если сделать электростатическое притяжение между атомами и сбрасывать скорость до нуля несколько раз, то формируется структура, похожая на каплю жидкости, но она двигается по экрану туда-сюда с рандомной скоростью, то быстрее то медленнее. Поэтому я не стал включать это в статью.
DigLik_228653
Полуявный метод Эйлера вычисляет сперва новую скорость, а затем новую позицию на основе новой скорости — в отличие от явного метода Эйлера, где и позиция, и скорость вычисляются исключительно по текущим значениям. Рекомендую изучить метод Хойна: он намного лучше аппроксимирует кривую скорости. Фактически метод Эйлера не вычисляет площадь под кривой, а вычисляет площадь прямоугольника под ней. Метод же Хойна считает площадь трапеции, что даёт заметно более точный результат.