Pymunkメモ

準備

  • 内部的にはChipmunk(Cベースの2次元物理エンジン)を使用している。Rubyなども公式サポートされている。
  • 類似のライブラリとして(Py)Box2Dがあるが,ドキュメントはPymunkの方が良い印象。
  • 画面表示にはPygame, Pyglet, Matplotlibが使える模様。
$ pip install pymunk

基礎知識

単位系は無い

  • Pymunkでシミュレーションをする時にまず悩むのが,各性質の単位。例えば,massはgなのかkgなのか?など。でも,これは悩む必要はない。というのも,Pymunkでは単位は無い。自分で想定して設定するしか無い。バネなどの制約は実際にやってみながら値を設定することになる。

画面座標

  • スクリーンサイズはW, Hの順で与える。NumpyなどはH,Wの順なのでちょっと注意が注意。
  • 左下が(0, 0),そこからxは右に+,yは上に+。Pygameは左上が(0,0)なので注意(ただし,debug_printでPygameを指定した場合は良きに変換してやってくれる)

BodyとShape

  • Chipmunk(を含む多くの物理エンジン)の重要な構成要素は,剛体(Rigid body),衝突形状(Collision shape), 関節/ジョイント(制約),空間。はじめは,剛体に形状情報を含むと勘違いしてしまうが,剛体自体は質量,位置,回転向き,速度などからなり,形は含まない。
  • 剛体の運動のシミュレーションでは,形は持たず,質量,位置,慣性などが理想的な状態を仮定してシミュレーションする。その際に利用されるのがBodyクラス。そして,それと並行してShapeクラスを用いて(衝突を含むシミュレーションでは)衝突判定を行う。

  • Bodyは6つの特徴から記述される。

    1. mass(質量)
    2. moment(慣性)
    3. position(位置)
    4. angle(向き)
    5. verocity(速度) *速度自体も向きを持つ
    6. angular_verocity(角速度)

Body(ボール)の作成例

  • 大まかな流れは下記になる。

    1. Bodyを生成。この際に,質量だけでなくモーメントを設定する。
    2. 初期位置(position)を設定する。
    3. 衝突シミュレーションの必要があれば,形状shapeを設定する(絶対に必要?)。
    4. シミュレーション空間に追加。
  • 単純なオブジェクトの場合には,予め定義された関数も用いてモーメントを設定可能。例えば,円の場合には,moment_for_circle。

b1 = pymunk.Body(mass=1, moment=10)
b1.position = 100, 100
s1 = pymunk.Circle(b1, radius=20)
space.add(b1, s1, ground

Vec2d

-2D空間上で位置(position)や向き(vector)を表すための専用クラスVec2dが提供されている。

ImpulseとForceの違い

  • 2つのAPI(apply_forceとapply_impulse)がある。forceとimpulseの違いは,impulseは大きな力が短い時間に与えられるケース,例えばボールを打ったり,壁に衝突したり,キャノンから発射されたり,というようなケース。このようなケースでは速度が直接変化させられる。(逆に,forceは速度は直接変化させられずに,f=maに従って加速度を変化させるものだと想像される。)

Template

  • 最も単純なケースとして,ボール1つが固定された地面に落ちる場合を載せておく。
  • Pygameの画面を保存する方法はシミュレーションは遅くなるけれど,pygame.image.save(screen, name)でスクリーンを画像として保存して,あとでffmpeg, imagemagickなどでgif化するのが手っ取り早い。
import sys

import pymunk
import pymunk.pygame_util
import pygame as pg
from pygame.locals import QUIT, KEYDOWN, K_ESCAPE


SCR_W, SCR_H = 400, 200
TICK = 100.


def setup_space():
    space = pymunk.Space()
    space.gravity = 0, -100
    space.damping = 0.99
    return space


def setup_pygame():
    # Initialize pygame screen for pymunk debug.
    pg.init()
    scr = pg.display.set_mode((SCR_W, SCR_H))
    clk = pg.time.Clock()
    draw_options = pymunk.pygame_util.DrawOptions(scr)
    return scr, clk, draw_options


def add_ground(space):
    b0 = space.static_body
    ground = pymunk.Segment(b0, (0, 0), (SCR_W, 0), 4)
    ground.elasticity = 1.0
    space.add(ground)
    return


def add_ball(space):
    b = pymunk.Body(mass=1, moment=10)
    b.position = 100, 100
    b.apply_impulse_at_local_point((30, 80))
    s = pymunk.Circle(b, radius=20)
    s.elasticity = 1.0
    space.add(b, s)
    return


def run():
    # Initialize Pygame and Pymunk
    scr, clk, draw_options = setup_pygame()  # Pygame setup
    space = setup_space()  # Pymunk setup

    # Add objects
    add_ground(space)
    add_ball(space)

    # Run simulation
    t = 0
    while True:
        for e in pg.event.get():
            if e.type == QUIT:
                sys.exit(0)
            elif e.type == KEYDOWN and e.key == K_ESCAPE:
                sys.exit(0)
        scr.fill((20, 20, 20))
        space.debug_draw(draw_options)
        space.step(1/TICK)
        pg.display.flip()
        clk.tick(TICK)
        pg.image.save(scr, f'./images/scr_{t:04}.png')
        t += 1
    pg.quit()


if __name__ == '__main__':
    run()