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つの特徴から記述される。
- mass(質量)
- moment(慣性)
- position(位置)
- angle(向き)
- verocity(速度) *速度自体も向きを持つ
- angular_verocity(角速度)
Body(ボール)の作成例
大まかな流れは下記になる。
- Bodyを生成。この際に,質量だけでなくモーメントを設定する。
- 初期位置(position)を設定する。
- 衝突シミュレーションの必要があれば,形状shapeを設定する(絶対に必要?)。
- シミュレーション空間に追加。
単純なオブジェクトの場合には,予め定義された関数も用いてモーメントを設定可能。例えば,円の場合には,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()