Box2dとwxPythonで2D物理シミュレーション

強化学習もgymやDeepMindの環境が出そろって、来年も熱くなりそうな予兆を見せていますが、やっぱり自分の手短な(ロボット制御)問題を解かせたくなることは多くないですか？

[ライントレーサーの環境をOpenAI I/F的にした \- Qiita](http://qiita.com/chachay/items/aa7b94f22163905b4c2b)では、物理則を無視したライントレーサが動き回っていましたが、今後、タイヤグリップや重力加速度を考慮した問題に取り組んでいくには、物理則をシミュレーションに組み込まないといけません。

ロボットシミュレータには、Gazebo, Webotなど3Dのものがいろいろ出回っていますが、PoC向けには2Dのほうが扱いが軽く、概念的に試せるスピードが早いので、PoC向けには、まずはこちらを推したいと個人的には思っています。

そんなわけで一時期、流行したBox2Dで物理シミュレーションと、（私にとっては）定番のwxPythonでアニメーション。

![2016-12-18.png](https://qiita-image-store.s3.amazonaws.com/0/117379/f9a6aab1-4928-0c8c-226b-26d5e90d7773.png)


# Box2dの導入

相変わらずWindowsといういけてない環境を使っている私はまずSwigの準備が必要でした ⇒ [WindowsにSWIG導入 \- Volo di notte](http://chachay.hatenablog.com/entry/2016/12/18/090010)

で、[pybox2d](https://github.com/pybox2d/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](https://www.microsoft.com/en-us/download/details.aspx?id=44266)
* python3の人 : [Download the Visual C\+\+ Build Tools \(standalone C\+\+ compiler, libraries and tools\)](http://landinghub.visualstudio.com/visual-cpp-build-tools)

# wxPythonの導入

最近Python3に切り替えたので、wxPythonのインストールをメモ書きします。
[Index of /Phoenix/snapshot\-builds](https://www.wxpython.org/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

まずシミュレーションのスケールファクターと時間を決めます

```python
PPM = 20.0  # pixels per meter
TimeStep = 20 # ms
```

つかうパッケージはこれだけ

```python
# -*- coding: utf-8 -*-
import Box2D 
from Box2D.b2 import (world, polygonShape, staticBody, dynamicBody)
import wx
```

## wxPythonの初期化

これまでのChachayシリーズと変わりませんが、wx.Appクラスをベースに下記構成で作っていきます。

```python
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).

```python
        # 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でも何でも好きなもの使い放題。

箱が空中から落ちてきて地面に着地するシミュレーションになります。

```python
    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)
```

# ソース全容

```python
# -*- 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()
```