強化学習もgymやDeepMindの環境が出そろって、来年も熱くなりそうな予兆を見せていますが、やっぱり自分の手短な(ロボット制御)問題を解かせたくなることは多くないですか?
ライントレーサーの環境をOpenAI I/F的にした - Qiitaでは、物理則を無視したライントレーサが動き回っていましたが、今後、タイヤグリップや重力加速度を考慮した問題に取り組んでいくには、物理則をシミュレーションに組み込まないといけません。
ロボットシミュレータには、Gazebo, Webotなど3Dのものがいろいろ出回っていますが、PoC向けには2Dのほうが扱いが軽く、概念的に試せるスピードが早いので、PoC向けには、まずはこちらを推したいと個人的には思っています。
そんなわけで一時期、流行したBox2Dで物理シミュレーションと、(私にとっては)定番のwxPythonでアニメーション。
Box2dの導入
相変わらずWindowsといういけてない環境を使っている私はまずSwigの準備が必要でした ⇒ WindowsにSWIG導入 - Volo di notte
で、pybox2dをクローンしてインストールしましょう!
python setup.py build
python setup.py install
Windowsの人はVC++のビルドツールとか必要な場合があります。チェック。
- python2の人 : Download Microsoft Visual C++ Compiler for Python 2.7 from Official Microsoft Download Center
- python3の人 : Download the Visual C++ Build Tools (standalone C++ compiler, libraries and tools)
wxPythonの導入
最近Python3に切り替えたので、wxPythonのインストールをメモ書きします。
Index of /Phoenix/snapshot-buildsから、" https://www.wxpython.org/Phoenix/snapshot-builds/wxPython_Phoenix-3.0.3.dev2700+c524ed1-cp35-cp35m-win_amd64.whl "など自分に該当するホイールパッケージのリンクを拾ってきて、
pip install https://www.wxpython.org/Phoenix/snapshot-builds/wxPython_Phoenix-3.0.3.dev2700+c524ed1-cp35-cp35m-win_amd64.whl
この辺の環境構築の手軽さがpython最高ですね。
Let's box2D
まずシミュレーションのスケールファクターと時間を決めます
PPM = 20.0 # pixels per meter
TimeStep = 20 # ms
つかうパッケージはこれだけ
# -*- coding: utf-8 -*-
import Box2D
from Box2D.b2 import (world, polygonShape, staticBody, dynamicBody)
import wx
wxPythonの初期化
これまでのChachayシリーズと変わりませんが、wx.Appクラスをベースに下記構成で作っていきます。
class APPWINDOW(wx.Frame):
def __init__(self, parent=None, id=-1, title=None):
# 1. 描画ウィンドウの初期化、タイマー初期化
# 2. CloseWindowバインド
# 3. 物理エンジンの初期化 + 物理オブジェクト(地面とか物体とか)の定義
# 4. タイマーループ開始
def CloseWindow(self, event):
# 単純な終了処理
def OnTimer(self, event):
# 1. ウィンドウをきれいに消去
# 2. 物理オブジェクトの描画
# 3. 物理シミュレーション1step分 実施
物理エンジン初期化
まずworldに重力を定義して、地面を作成(CreateStaticBody)、さらに空中に箱を配置(CreateDynamicBody).
# pybox2d world
self.phys_world = world(gravity=(0, -10), doSleep=True)
self.ground_body = self.phys_world.CreateStaticBody(
position=(0, 1),
shapes=polygonShape(box=(50, 5)),
)
self.dynamic_body = self.phys_world.CreateDynamicBody(position=(10, 15), angle=15)
self.box = self.dynamic_body.CreatePolygonFixture(box=(2, 1), density=1, friction=0.3)
タイマーループ処理
ここまで定義してやるとシミュレーション超簡単。Step関数呼ぶだけ。
あとは絵を描くところだけ工夫。アニメーションはkivyでも何でも好きなもの使い放題。
箱が空中から落ちてきて地面に着地するシミュレーションになります。
def OnTimer(self, event):
# 1. ウィンドウをきれいに消去
self.bdc = wx.BufferedDC(self.cdc, self.bmp)
self.gcdc = wx.GCDC(self.bdc)
self.gcdc.Clear()
self.gcdc.SetPen(wx.Pen('white'))
self.gcdc.SetBrush(wx.Brush('white'))
self.gcdc.DrawRectangle(0,0,640,640)
# 2. 物理オブジェクトの描画
for body in (self.ground_body, self.dynamic_body): # or: world.bodies
for fixture in body.fixtures:
shape = fixture.shape
vertices = [(body.transform * v) * PPM for v in shape.vertices]
vertices = [(int(v[0]), int(480 - v[1])) for v in vertices]
self.gcdc.SetPen(wx.Pen(wx.Colour(50,50,50)))
self.gcdc.SetBrush(wx.Brush(wx.Colour(colors[body.type])))
self.gcdc.DrawPolygon(vertices)
# 3. 物理シミュレーション1step分 実施
self.phys_world.Step(TimeStep/1000, 10, 10)
ソース全容
# -*- coding: utf-8 -*-
import Box2D
from Box2D.b2 import (world, polygonShape, staticBody, dynamicBody)
import wx
PPM = 20.0 # pixels per meter
TimeStep = 20 # ms
colors = {
staticBody: (150,150,150),
dynamicBody: (112,146,190),
}
class APPWINDOW(wx.Frame):
def __init__(self, parent=None, id=-1, title=None):
# 1. 描画ウィンドウの初期化、タイマー初期化
wx.Frame.__init__(self, parent, id, title)
self.MainPanel = wx.Panel(self, size=(640, 480))
self.MainPanel.SetBackgroundColour('WHITE')
self.panel = wx.Panel(self.MainPanel, size = (640,480))
self.panel.SetBackgroundColour('WHITE')
mainSizer = wx.BoxSizer(wx.VERTICAL)
mainSizer.Add(self.panel)
self.SetSizer(mainSizer)
self.Fit()
self.timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.OnTimer)
self.cdc = wx.ClientDC(self.panel)
w, h = self.panel.GetSize()
self.bmp = wx.Bitmap(w,h)
# 2. CloseWindowバインド
self.Bind(wx.EVT_CLOSE, self.CloseWindow)
# 3. 物理エンジンの初期化 + 物理オブジェクト(地面とか物体とか)の定義
self.phys_world = world(gravity=(0, -10), doSleep=True)
self.ground_body = self.phys_world.CreateStaticBody(
position=(0, 1),
shapes=polygonShape(box=(50, 5)),
)
self.dynamic_body = self.phys_world.CreateDynamicBody(position=(10, 15), angle=15)
self.box = self.dynamic_body.CreatePolygonFixture(box=(2, 1), density=1, friction=0.3)
# 4. タイマーループ開始
self.timer.Start(TimeStep)
def CloseWindow(self, event):
# 単純な終了処理
wx.Exit()
def OnTimer(self, event):
# 1. ウィンドウをきれいに消去
self.bdc = wx.BufferedDC(self.cdc, self.bmp)
self.gcdc = wx.GCDC(self.bdc)
self.gcdc.Clear()
self.gcdc.SetPen(wx.Pen('white'))
self.gcdc.SetBrush(wx.Brush('white'))
self.gcdc.DrawRectangle(0,0,640,640)
# 2. 物理オブジェクトの描画
for body in (self.ground_body, self.dynamic_body): # or: world.bodies
for fixture in body.fixtures:
shape = fixture.shape
vertices = [(body.transform * v) * PPM for v in shape.vertices]
vertices = [(int(v[0]), int(480 - v[1])) for v in vertices]
self.gcdc.SetPen(wx.Pen(wx.Colour(50,50,50)))
self.gcdc.SetBrush(wx.Brush(wx.Colour(colors[body.type])))
self.gcdc.DrawPolygon(vertices)
# 3. 物理シミュレーション1step分 実施
self.phys_world.Step(TimeStep/1000, 10, 10)
if __name__ == '__main__':
app = wx.App()
w = APPWINDOW(title='Box2D Test')
w.Center()
w.Show()
app.MainLoop()