Pythonで物理シミュレーションをはじめよう ~Pybullet入門~


## はじめに
![my_simulation-2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/909214/00e60860-50e0-e756-a9b2-637c5b55fe52.gif)

**「Pythonで、ちゃんとした物理エンジンを動かしたい！」** という願いを叶えてくれるのがPybulletです。

少し使うのが難しいですが
特徴として


* オープンソースである
* ロボットのシミュレーションも行える　(URDFなども読み込める)
* 深度画像、セグメンテーション画像なども取得可能、raytestなども利用可能
* 強化学習用の環境が一部提供されている

などが挙げられ、物理シミュレーション、ロボティクス、AIなど様々な分野で利用できます。
（実際に研究者も使っているツールです）



[公式サイト](https://pybullet.org/wordpress/)
[クイックスタートガイド](https://docs.google.com/document/d/10sXEhzFRSnvFcl3XxNGhnD4N2SedqwdAvK3dsihxVUA/edit#heading=h.2ye70wns7io3)
[Git Hub](https://github.com/bulletphysics/bullet3)


## 今回の実行環境について
Google Colaboratoryで実行しています。

ランタイムはGPUの方がいいですが、CPUでも動きます。
(CPUだと最初のpipインストールが10分くらいかかる)

ローカルのノートPCなどで実行しようとするとかなり重たいですが、UIで動かせるという利点があります。


## インストール、インポート
Colabでやっているのでコマンドの前に「!」がついていますが環境に応じて変えてください

インストール

```
!pip install pybullet
```

インポート

```python
import pybullet
```

gitのクローン
強化学習用の環境やロボットのモデル(URDFファイルなど)が読み込める。
(URDF、SDF、Objなどの形式でR2D2,　Kukaのロボットアーム, レースカーなど色々なものが用意されている)


```
!git clone https://github.com/bulletphysics/bullet3.git
```

色々な設定で使うのでこちらもインポート
```python
import pybullet_data
```

## 物理エンジンへの接続

今回はColabでやっているので
```python
physicsClient = pybullet.connect(pybullet.DIRECT) 
```

もしローカルで実行する場合は
```python
physicsClient = pybullet.connect(pybullet.GUI) 
```

## 環境の設定
基本設定
```python
pybullet.setAdditionalSearchPath(pybullet_data.getDataPath())
```


重力を設定する
```python
pybullet.setgravity(gravx,gravy,gravz)
```

gravx: x軸方向の重力
gravy: y軸方向の重力
gravz: z軸方向の重力
(全てfloat)

基本的には(地球上において)

```python
pybullet.setgravity(0,0,-9.8)
```

## 物体のロード

URDF(Universal Robot Description File)のファイルを読み込める変形しやすい物体(布など)も読み込める
URDFはROSなどで用いられる

```python
object=pybullet.loadURDF('~~~.urdf')
```

loadSDF:SDFファイルを読み込める
loadMJCF:MuJoCo MJCFのxmlを読み込める


VTK や OBJ 形式の変形しやすい物体(布など)を読み込める

```python
object=pybullet.loadSoftBody('.obj')
```



### カメラ画像の受け取り

RGB、Depth、セグメンテーション画像が受け取れます。AIの学習(特に強化学習)をする際に非常に便利な機能です。


```python
width, height, rgbPixels, depthPixels, segmentationMaskBuffer = pybullet.getCameraImage(width, height, ...)

```

RGB画像(rgbPixels)
![スクリーンショット 2021-04-05 13.36.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/909214/9bb33e5b-adbe-5581-c731-f32644c418c5.png)


深度(Depth)カメラ映像(depthPixels)
![スクリーンショット 2021-04-05 13.37.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/909214/6123abcb-b03e-2e55-954b-5cce7683b3b3.png)


セグメンテーションマスク(segmentationMaskBuffer)
![スクリーンショット 2021-04-05 13.38.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/909214/1198906a-f08c-b821-e848-aae55affef14.png)


## カメラ設定



getCameraImageの引数


```python
pybullet.getCameraImage(width, height, viewMatrix , projectionMatrix, lightDirection, lightColor, lightDistance, shadow, lightAmbientCoeff, lightDiffuseCoeff, lightSpecularCoeff, renderer)

```


|主なパラメーター|意味 |
|--|--|
|width|画像の横幅|
|height|画像の高さ|
|viewMatrix|カメラの位置、方向など(下記参照)|
|projectionMatrix|カメラの撮影設定(下記参照)|
| lightDirection | ライトの方向ベクトル lightDirection= (x,y,z) |
| lightColor | ライトの色　lightColor=(R,G,B) |
|lightDistance|ライトの距離|
|shadow|影をつけるかどうか: 1→影をつける 0→影をつけない|


### カメラの位置、方向など

```python
viewMatrix = pybullet.computeViewMatrix(cameraEyePosition=[0, 0, 2],cameraTargetPosition=[0, 0, 0],cameraUpVector=[0, 1, 0])
```
|主なパラメーター|意味 |
|--|--|
cameraEyePosition| カメラの位置
cameraTargetPosition| カメラが写す中心座標
cameraUpVector| カメラの上面の法線ベクトル※

※カメラが中心座標をどのくらい見下ろすor見上げるのかを決める

### カメラの撮影設定
```python
projectionMatrix = pybullet.computeProjectionMatrixFOV(fov=45.0,aspect=1.0,nearVal=0.1,farVal=3.1)
```
|主なパラメーター|意味 |
|--|--|
fov| カメラが写す範囲の広さ
aspect| アスペクト(画像の縦横比)
nearVal| カメラが写す最小の距離
farVal| カメラが写す最大の距離




## シミュレーション結果の表示・保存


下のコードを実行すると、3次元直交座標(0,0,3)から、重力加速度9.8でヒューマノイドが自由落下するシミュレーション結果をGIFファイルで保存できます。


```python
from PIL import Image

pybullet.resetSimulation()
pybullet.setAdditionalSearchPath(pybullet_data.getDataPath())
pybullet.setGravity(0,0,-9.8)
timestep = 1. / 240.
pybullet.setTimeStep(timestep)
floor = pybullet.loadURDF("plane.urdf")

startposition = [0,0,3]  # x,y,z
startorient = pybullet.getQuaternionFromEuler([0,0,3.14])  
Humanoid = pybullet.loadURDF("./bullet3/data/humanoid/nao.urdf",startposition, startorient) 
 # 記事冒頭のボールを落下させるシミュレーションでは "./bullet3/data/sphere2.urdf"を指定

frame = []
for t in range (400):
    pybullet.stepSimulation()
    if t % 8 == 0:
        width, height, rgbImg, depthImg, segImg = pybullet.getCameraImage(360,240)
        frame.append(rgbImg)

images =[]
for im in frame:
  img = Image.fromarray(im)
  images.append(img)
images[0].save('my_simulation.gif',save_all=True, append_images=images[:], optimize=False, duration=40, loop=0)
```
### 実行結果

![my_simulation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/909214/bc2ab9f1-5720-be8c-2b0e-f8f6b8f0755b.gif)

ヒューマノイドの設定(色、コリジョンなど)はURDFを編集することで変更できます。




## 最後に

今回は、Pybulletの基本的な使い方を紹介しました。

自分もまだわからない部分が多いので、たくさん使って勉強していきたいと思います。

間違いなどがあれば遠慮なくコメントで指摘をお願いします。

以下の関連記事では実際にPybulletでロボットを動かす方法を解説しています
![Racecar_moving1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/909214/0c5cfff5-ba76-13ad-e1de-e1e947c80aca.gif)


https://qiita.com/KamikawaTakato/items/c13c1440389ae7d5f219