Физика. Кто-то её любит, кто-то нет, но определённо это неотъемлемая часть нашего существования. В этой статье мы рассмотрим как самому создавать физические симуляции используя всего 2 библиотеки Python.

К концу статьи мы сделаем интерактивную симуляцию взаимодействия тел и поймём основы использования библиотеки Pymunk.

Моя терминология:

Раздел - кусок кода который я помечаю комментарием, чтобы описать это в статье
(В качестве термина не используется, но мне так проще вам объяснить)


Подготовимся для начала работы

И так, создадим новый файл Python в удобной для вас IDE

Теперь нам нужно установить Pymunk и Pygame. А так же настроить их для совместной работы.
Не забудьте установить их в консоли проекта через команду pip install

Вот код:

#импорт модулей
import pygame as pg
import pymunk.pygame_util

Система координат PyGame отличается от системы координат PyMunk, нам нужно подогнать их под одну систему координат

PyGame
PyGame
PyMunk
PyMunk

Добавим код в "импорт модулей" для исправления этого

pymunk.pygame_util.positive_y_is_up = False

Ура, мы подогнали систему координат и теперь наши библиотеки могут работать друг с другом.

Отрисовка в Pygame

Чтобы визуализировать нашу симуляцию мы будем использовать не сложный код

Зададим разрешение экрана и количество FPS.

#Настройки PyGame
RES = WIDTH, HEIGHT = 900, 720
FPS = 60

pg.init()
surface = pg.display.set_mode(RES)
clock = pg.time.Clock()
draw_options = pymunk.pygame_util.DrawOptions(surface)

WIDTH и HEIGHT это параметры разрешения окна Pygame.

FPS - это количество кадров обрисовываемые в секунду (стандартом считается 60)

Теперь создадим цикл отрисовки. Он будет всегда находиться в самом конце кода.

while True:
    surface.fill(pg.Color('black'))

    for i in pg.event.get():
        if i.type == pg.QUIT:
            exit()
    pg.display.flip()
    clock.tick(FPS)

Код на данный момент выглядит так:

#имопрт модулей
import pygame as pg
import pymunk.pygame_util
pymunk.pygame_util.positive_y_is_up = False

#Настройки PyGame
RES = WIDTH, HEIGHT = 900, 720
FPS = 60

pg.init()
surface = pg.display.set_mode(RES)
clock = pg.time.Clock()
draw_options = pymunk.pygame_util.DrawOptions(surface)

#Отрисовка PyGame
while True:
    surface.fill(pg.Color('black'))

    for i in pg.event.get():
        if i.type == pg.QUIT:
            exit()
            
    pg.display.flip()
    clock.tick(FPS)

Наконец мы можем посмотреть что выходит.

Наше окно готово к созданию симуляции.

Создаём Пространство

Но для начала разберёмся с основными понятиями библиотеки PyMunk.

Для начала нам нужно создать пространство (space)
Space - основная единица моделирования PyMunk. Вы добавляете к нему твердые тела, формы и суставы, а затем продвигаете их вперед вместе во времени.

Создаём новый комментарий "Переменные PyMunk" который будет находиться после нашего раздела "Настройки PyGame" и создаём там space

#переменные Pymunk
space = pymunk.Space()

Пространство есть, пора создать гравитацию.

space.gravity = 0, 8000

Теперь наш раздел "Переменные PyMunk" выглядит так:

#настройки Pymunk
space = pymunk.Space()
space.gravity = 0, 8000

После добавления нужных переменных важно сделать следующее:

Добавим пару строк в отрисовку чтобы она рисовала наши тела созданные в PyMunk:

#Отрисовка
while True:
    surface.fill(pg.Color('black'))

    for i in pg.event.get():
        if i.type == pg.QUIT:
            exit()
#Мы добавили вот эти 2 строки:
    space.step(1 / FPS)
    space.debug_draw(draw_options)

    pg.display.flip()
    clock.tick(FPS)

Код на данный момент выглядит так:

#импорт модулей:
import pygame as pg
from random import randrange
import pymunk.pygame_util
pymunk.pygame_util.positive_y_is_up = False

#параметры PyGame
RES = WIDTH, HEIGHT = 900, 720
FPS = 60

pg.init()
surface = pg.display.set_mode(RES)
clock = pg.time.Clock()
draw_options = pymunk.pygame_util.DrawOptions(surface)

#настройки Pymunk
space = pymunk.Space()
space.gravity = 0, 8000

#Отрисовка
while True:
    surface.fill(pg.Color('black'))

    for i in pg.event.get():
        if i.type == pg.QUIT:
            exit()

    space.step(1 / FPS)
    space.debug_draw(draw_options)

    pg.display.flip()
    clock.tick(FPS)

Создаём физические тела

В PyMunk есть 3 типа физических объектов: Динамические, Статические и Кинематические

Динамические (Dynamic) - Объекты реагируют на столкновения, на них действуют силы и гравитация, они имеют конечную массу.
Динамические тела взаимодействуют со всеми типами тел.

Кинематические (Kinematic) - Это объекты, которые управляются из вашего кода, а не внутри физического движка. На них не действует гравитация. Хорошие примеры кинематических тел: летающие платформы в играх

Статические (Static) - Тела которые никогда не двигаются, но могут взаимодействовать с другими телами.
Хороший пример - стены и полы в играх.

Создадим Статическую платформу:

Пишем вот такой код после раздела "настройки Pymunk"

#платформа
segment_shape = pymunk.Segment(space.static_body, (1, HEIGHT), (WIDTH, HEIGHT), 26)
space.add(segment_shape)
segment_shape.elasticity = 0.4
segment_shape.friction = 1.0

space.add(названия объектов через запятую) - добавляет объекты в пространство

segment_shape.elasticity - Коэффициент упругости

segment_shape.friction - Коэффициент трения

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

Интерактивные квадраты и их взаимодействие

Вы добрались до ключевого раздела статьи, поздравляю!
Сейчас мы разберёмся как создавать динамические тела и как сделать для них правильное взаимодействие.

Идея:
Кликая на экран программа будет добавлять новые квадраты поддающиеся законам физики.

Приступаем к реализации:

#квадратики
def create_square(space, pos):
    square_mass, square_size = 1, (60, 60)
    square_moment = pymunk.moment_for_box(square_mass, square_size)
    square_body = pymunk.Body(square_mass, square_moment)

square_mass - масса квадрата (в наше случае равняется одному)

square_size - размер квадрата. Передаётся в формате (width, height)

square_moment = pymunk.moment_for_box - автоматически рассчитываем момент инерции для box зная массу и размер

quare_body = pymunk.Body - экземпляр тела

Время для первой фичи: Cделаем так, чтобы квадраты появлялись на месте где стоит курсор

def create_square(space, pos):
    square_mass, square_size = 1, (60, 60)
    square_moment = pymunk.moment_for_box(square_mass, square_size)
    square_body = pymunk.Body(square_mass, square_moment)
    # появление на позиции курсора
    square_body.position = pos

square_body.position - позиция тела

def create_square(space, pos):
    square_mass, square_size = 1, (60, 60)
    square_moment = pymunk.moment_for_box(square_mass, square_size)
    square_body = pymunk.Body(square_mass, square_moment)
    square_body.position = pos
    square_shape = pymunk.Poly.create_box(square_body, square_size)
    square_shape.elasticity = 0.8
    square_shape.friction = 1.0

Фича под номером два: цвет кубиков будет рандомен.
Сделать это можно так:

square_shape.color = [randrange(256) for i in range(4)]

Наконец дописываем функцию, в итоге она выглядит так:

def create_square(space, pos):
    square_mass, square_size = 1, (60, 60)
    square_moment = pymunk.moment_for_box(square_mass, square_size)
    square_body = pymunk.Body(square_mass, square_moment)
    square_body.position = pos
    square_shape = pymunk.Poly.create_box(square_body, square_size)
    square_shape.elasticity = 0.8
    square_shape.friction = 1.0
    square_shape.color = [randrange(256) for i in range(4)]
    space.add(square_body, square_shape)

напоминаю что space.add добавляет тело в пространств

Мы на финишной прямой! Осталось только обновить отрисовку.

# спавн кубиков
        if i.type == pg.MOUSEBUTTONDOWN:
            if i.button == 1:
                create_square(space, i.pos)
                print(i.pos)

В итоге отрисовка выглядит так:

#Отрисовка
while True:
    surface.fill(pg.Color('black'))

    for i in pg.event.get():
        if i.type == pg.QUIT:
            exit()
        # спавн мячиков
        if i.type == pg.MOUSEBUTTONDOWN:
            if i.button == 1:
                create_square(space, i.pos)
                print(i.pos)

    space.step(1 / FPS)
    space.debug_draw(draw_options)

    pg.display.flip()
    clock.tick(FPS)

print('end')

Проверяем

Считаю что получилось отлично :)

Результат
Результат

Весь исходный код:

#импорт модулей:
import pygame as pg
from random import randrange
import pymunk.pygame_util
pymunk.pygame_util.positive_y_is_up = False

#параметры PyGame
RES = WIDTH, HEIGHT = 900, 720
FPS = 60

pg.init()
surface = pg.display.set_mode(RES)
clock = pg.time.Clock()
draw_options = pymunk.pygame_util.DrawOptions(surface)

#настройки Pymunk
space = pymunk.Space()
space.gravity = 0, 8000

#платформа
segment_shape = pymunk.Segment(space.static_body, (2, HEIGHT), (WIDTH, HEIGHT), 26)
space.add(segment_shape)
segment_shape.elasticity = 0.8
segment_shape.friction = 1.0



#квадратики
body = pymunk.Body()
def create_square(space, pos):
    square_mass, square_size = 1, (60, 60)
    square_moment = pymunk.moment_for_box(square_mass, square_size)
    square_body = pymunk.Body(square_mass, square_moment)
    square_body.position = pos
    square_shape = pymunk.Poly.create_box(square_body, square_size)
    square_shape.elasticity = 0.4
    square_shape.friction = 1.0
    square_shape.color = [randrange(256) for i in range(4)]
    space.add(square_body, square_shape)


#Отрисовка
while True:
    surface.fill(pg.Color('black'))

    for i in pg.event.get():
        if i.type == pg.QUIT:
            exit()
        # спавн кубиков
        if i.type == pg.MOUSEBUTTONDOWN:
            if i.button == 1:
                create_square(space, i.pos)
                print(i.pos)

    space.step(1 / FPS)
    space.debug_draw(draw_options)

    pg.display.flip()
    clock.tick(FPS)

print('end')

Итог

Мы создали небольшую программу на PyGame и PyMunk с симуляцией физики.

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

До связи)

Материалы: Standalone Coder, форумы, официальная документация.

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


  1. forthuser
    06.12.2021 01:16
    +1

    Такая же близко демо симуляция в проекте с расширением DSL (-ем) JavaScript языка.

    В браузере Chipmunk Physics Engine demo — Pyramid Stack

    image

    P.S. Из этого проекта, где ещё представлены некоторые демо симуляции физических моделей


    1. Nikuson Автор
      06.12.2021 01:24

      Да, можно не только на Python.

      но я выбрал его чтобы это была первая программа с физикой для начинающих, а Python сейчас очень популярен как первый язык.


      1. forthuser
        06.12.2021 01:34
        +1

        Умение использования готовой библиотечной реализации Физической модели не сильно проясняет суть происходящих упругих взаимодействий тел в модели. ????


        1. Nikuson Автор
          06.12.2021 02:20

          я более-менее пытался, следующую статью постараюсь сделать находчивее и лучше.


          1. Nikuson Автор
            06.12.2021 02:24

            там как раз тема где нужно подробно объяснять


  1. ARad
    06.12.2021 06:06
    +2

    Похоже физика не сильно точная, столкновения или неправильно считаются или кубики могут проникать друг в друга, а затем восстанавливать форму.


    1. lab412
      06.12.2021 08:15
      +2

      это типичная проблема сэмплирования времени. в первом кадре объект не долетел, а во втором уже внутри другого. надо просто увеличить количеству "субкадров", ну то есть считать время не в 60ти кадрах при 60FPS а в 120 или 240. в итоге всё равно произойдет проникновение так как "физика" считается лишь постфактум - сместили объект и посчитали что будет дальше происходить. просто при большем количестве промежуточных шагов получается меньше "ошибок". на самом деле при большой скорости движения может произойти и "тунелирование" объектов когда за время между сэмплами объект полностью пройдет через другой оказавшись на другой стороне


      1. bfDeveloper
        06.12.2021 13:02
        +2

        Это уже давным-давно научились делать правильно. Вместо тел в геометрии поиска столкновений используются вытянутые по их скорости "капсулы". Это позволяет определять все столкновения и высчитывать точный момент столкновения. И шаг времени не приходится избыточно уменьшать.

        И статические стенки из коробок тоже уже нормальные движки умеют держать стабильными без этих прыжков и взаимопроникновений. Выглядит как Box2D 10, если не 15 лет назад.


        1. QDeathNick
          06.12.2021 23:50

          Можете привести пример библиотеки или движка, где сделано правильно?


          1. bfDeveloper
            07.12.2021 01:02

            Про поиск столкновений - любой, заявляющий Continuous collision detection, например Unity, или тот же Box2D.

            Со стенками из кубиков всё сложнее, общее решение не очень практично, так как требует честного решения СЛАУ. Но небольшими хаками это тоже чинится, например ещё 13 лет назад в Havok стопки из кубиков были вполне стабильны. Если не ошибаюсь, то там было многопроходное решение столкновений.