Физика. Кто-то её любит, кто-то нет, но определённо это неотъемлемая часть нашего существования. В этой статье мы рассмотрим как самому создавать физические симуляции используя всего 2 библиотеки Python.
К концу статьи мы сделаем интерактивную симуляцию взаимодействия тел и поймём основы использования библиотеки Pymunk.
Моя терминология:
Раздел - кусок кода который я помечаю комментарием, чтобы описать это в статье
(В качестве термина не используется, но мне так проще вам объяснить)
Подготовимся для начала работы
И так, создадим новый файл Python в удобной для вас IDE
Теперь нам нужно установить Pymunk и Pygame. А так же настроить их для совместной работы.
Не забудьте установить их в консоли проекта через команду pip install
Вот код:
#импорт модулей
import pygame as pg
import pymunk.pygame_util
Система координат PyGame отличается от системы координат 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)
ARad
06.12.2021 06:06+2Похоже физика не сильно точная, столкновения или неправильно считаются или кубики могут проникать друг в друга, а затем восстанавливать форму.
lab412
06.12.2021 08:15+2это типичная проблема сэмплирования времени. в первом кадре объект не долетел, а во втором уже внутри другого. надо просто увеличить количеству "субкадров", ну то есть считать время не в 60ти кадрах при 60FPS а в 120 или 240. в итоге всё равно произойдет проникновение так как "физика" считается лишь постфактум - сместили объект и посчитали что будет дальше происходить. просто при большем количестве промежуточных шагов получается меньше "ошибок". на самом деле при большой скорости движения может произойти и "тунелирование" объектов когда за время между сэмплами объект полностью пройдет через другой оказавшись на другой стороне
bfDeveloper
06.12.2021 13:02+2Это уже давным-давно научились делать правильно. Вместо тел в геометрии поиска столкновений используются вытянутые по их скорости "капсулы". Это позволяет определять все столкновения и высчитывать точный момент столкновения. И шаг времени не приходится избыточно уменьшать.
И статические стенки из коробок тоже уже нормальные движки умеют держать стабильными без этих прыжков и взаимопроникновений. Выглядит как Box2D 10, если не 15 лет назад.
QDeathNick
06.12.2021 23:50Можете привести пример библиотеки или движка, где сделано правильно?
bfDeveloper
07.12.2021 01:02Про поиск столкновений - любой, заявляющий Continuous collision detection, например Unity, или тот же Box2D.
Со стенками из кубиков всё сложнее, общее решение не очень практично, так как требует честного решения СЛАУ. Но небольшими хаками это тоже чинится, например ещё 13 лет назад в Havok стопки из кубиков были вполне стабильны. Если не ошибаюсь, то там было многопроходное решение столкновений.
forthuser
Такая же близко демо симуляция в проекте с расширением DSL (-ем) JavaScript языка.
В браузере Chipmunk Physics Engine demo — Pyramid Stack
P.S. Из этого проекта, где ещё представлены некоторые демо симуляции физических моделей
Nikuson Автор
Да, можно не только на Python.
но я выбрал его чтобы это была первая программа с физикой для начинающих, а Python сейчас очень популярен как первый язык.
forthuser
Умение использования готовой библиотечной реализации Физической модели не сильно проясняет суть происходящих упругих взаимодействий тел в модели. ????
Nikuson Автор
я более-менее пытался, следующую статью постараюсь сделать находчивее и лучше.
Nikuson Автор
там как раз тема где нужно подробно объяснять