🐍

PyBulletの調査: 基本からモデル作成まで

2021/09/18に公開

1. PyBulletとは

1.1 概要

強化学習においてロボット等の物理シミュレーションには有償のMuJoCoがよく利用されていますが、無償のOSSシミュレータのPyBulletもあります。

このPyBulletはC++で書かれた物理シミュレータのBullet Physics SDK (通称Bullet3) のPythonバインディングです。
https://github.com/bulletphysics/bullet3

1.2 Bullet3 と PyBullet の位置づけ

元々Pythonのバインディングはおまけだったのか、PyBulletのコードは examples/pybullet というディレクトリに配置されています。
(一方で、GitHubに記載の公式サイト: bulletphysics.org は、pybullet.org にリダイレクトされるので、今の主流はPyBulletなんだろうなぁとも感じます。)

1.3 困ったら

GitHub上の issue は質問で溢れかえってしまったので閉鎖したとREADMEに記載があります。サイトに設置のフォーラムを利用してくださいとのことです。 (個人的には、この手の昔ながらのフォーラムは、慣れないせいか読みづらくてあまり好きに慣れないです。。。)

The Issue tracker was flooded with support questions and is closed until it is cleaned up. Use the PyBullet forums to discuss with others.

2. インストール / ビルド

2.1 PyBulletのインストール

PyBullet のシミュレーション環境をPythonから利用したいだけの一般ユーザーの場合、PyPIからインストールするのが簡単だと思います。
(コンパイル済みバイナリは Linux + Python2.7 向けにしか用意されていないので、いずれにしても手元でコンパイルすることになりますが、パッケージのインストールプロセスの中で自動的にコンパイルされます。)

https://pypi.org/project/pybullet/

インストールコマンド
pip install pybullet

2.2 Bullet3の手動ビルド

C++から利用するなどPyBulletではないBullet3を利用したい際は、ソースコードからビルドする必要があります。READMEにはいくつかの方法が書かれていますが、macOSやLinuxであれば cmake を用意して、build_cmake_pybullet_double.sh スクリプトを実行するのが簡単でしょう。

git clone https://github.com/bulletphysics/bullet3.git
cd bullet3
./build_cmake_pybullet_double.sh

3. 実装

3.1 Python I/F

Pythonからアクセスできる pybullet モジュールは examples/pybullet/pybullet.c に定義されています。

PythonとのI/Fは、Cythonpybind11 を利用することなく、CPython の C API をそのまま利用して実装されています。

(このファイルがかなり大きいのでI/Fのソースコードを読み解くだけでもかなり苦労しています。)

4. 使い方

(更新履歴が2017年末でちょっと古いので心配ですが) PyBullet の使い方は以下に記載されています。

https://github.com/bulletphysics/bullet3/blob/master/docs/pybullet_quickstart_guide/PyBulletQuickstartGuide.md.html

また、シミュレーションの大まかな流れは以下の記事も参考にさせていただきました。
https://qiita.com/Conny_Brown_jp/items/b09928bea3d63ce2a68d

4.1 物理エンジン接続

初めに物理エンジンに pybullet.connect 関数で接続します。
いくつかの種類があり、pybullet.GUI を渡すと X11 を利用したGUI付きの物理エンジン[1]が、pybullet.DIRECT だとGUIの無い物理エンジンが新しく作成されます。

(他にも、既存の物理エンジンを利用する方法として、pybullet.TCPpybullet.UDPpybullet.SHARED_MEMORY があります。)

この pybullet.connect は物理エンジンID (ただのint) を返してきます。1つしか物理エンジンを作成しなければ問題ないですが、このIDはユーザーの責任で管理しておく必要があります。

import pybullet

engine_id = pybullet.connect(pybullet.DIRECT)

4.2 シミュレーション条件の設定

4.2.1 重力

初期状態では、重力は指定されていないため、必要であれば pybullet.setGravity 関数で指定します。

# ユークリッド座標における各軸方向の重力加速度の大きさ
# 単位はおそらく m/s^2
gX = 0
gY = 0
gZ = -10

pybullet.setGravity(gX, gY, gZ, engine_id)

4.2.2 時間幅

また、シミュレーション1ステップの時間幅は(ドキュメントに記載がないですが) pybullet.setTimeStep関数で指定できます。

pybullet.setTimeStep(1 / 60, engine_id) # 単位はおそらく秒

4.3 モデル構築

モデルは、URDF形式、Bullet形式[2]SDF形式MJCF形式のファイルで作成して対応する関数(pybullet.loadURDF, pybullet.loadBullet, pybullet.loadSDF, pybullet.loadMJCF)で読み込むことが推奨されています。

どうやらいずれもXML形式の一種のようで、複雑なモデルを作成して共有するというニーズには合うのだと思いますが、ちょっとしたモデル作成をするには手間だと感じました。

ここでは非推奨の方法ですが、PyBulletのAPIからプログラムでモデルを作成する方法を簡単に記載しようと思います。

4.3.1 オブジェクトの(雛形)構築

Bulletで取り扱うことができる基本的なオブジェクトについては次のブログ記事が参考になります。
https://zerogram.info/blog/?p=2333

pybulletからは、 pybullet.createCollisionShape 関数で、衝突判定用のオブジェクトを、 pybullet.createVisualShape 関数で、(衝突判定とは無関係の)外見オブジェクトを作成します。(強化学習のコンテキストでは衝突判定と外見を分けることは不要と感じますが、ゲームではこれらをうまく組み合わせることで、当たり判定の小さなプレイヤーを構築したりできるので必要なのだと思います。)

両関数は、オブジェクト形状のタグによって形状を指定します。オブジェクトの形状ごとに追加で渡すことができるオプションが異なるので要注意です。

pybullet.createVisualShape 関数は、さらに追加オプションで rgbaColor (色・透明度)、specularColor (色ごとの反射率)、visualFramePosition (リンクしたフレームからの位置?)、 visualFrameOrientation (リンクしたフレームからの回転?) を指定できます。

sphere_cid = pybullet.createCollisionShape(pybullet.GEOM_SPHERE, radius=0.5, physicsClientId=engine_id)
sphere_vid = pybullet.createVisualShape(pybullet.GEOM_SPHERE, radius=0.5, physicsClientId=engine_id, rgbaColor=[0,0,1,1]) # 青・不透明

box_cid = pybullet.createCollisionShape(pybullet.GEOM_BOX, halfExtents=[1,1,1], physicsClientId=engine_id)
box_vid = pybullet.createVisualShape(pybullet.GEOM_BOX, halfExtents=[1,1,1], physicsClientId=engine_id, rgbaColor=[1,0,0,0.5]) # 赤・半透明

カプセル

capsul_cid = pybullet.createCollisionShape(pybullet.GEOM_CAPSULE, radius=0.5, height=1, physicsClientId=engine_id)
capsul_vid = pybullet.createVisualShape(pybullet.GEOM_CAPSULE, radius=0.5, height=1, physicsClientId=engine_id, rgbaColor=[0,1,0,1]) # 緑・不透明

円柱

cylinder_cid = pybullet.createCollisionShape(pybullet.GEOM_CYLINDER, radius=0.5, height=1, physicsClientId=engine_id)
cylinder_vid = pybullet.createVisualShape(pybullet.GEOM_CYLINDER, radius=0.5, height=1, physicsClientId=engine_id, rgbaColor=[1,1,1,1])

平面

plane_cid = pybullet.createCollisionShape(pybullet.GEOM_PLANE, planeNormal=[0,0,1], physicsClientId=engine_id)
plane_vid = pybullet.createVisualShape(pybullet.GEOM_PLANE, planeNormal=[0,0,1], physicsClientId=engine_id, rgbaColor=[1,1,1,1])

planeNormalは法線ベクトルを指定。

4.3.2 オブジェクトの設置 (インスタンス化)

4.3.1節で作成したオブジェクトの雛形を実際に物理モデルとして質量や位置・方向を決めて設置していくことになります。pybulletでは、pybullet.createMultiBody関数で設置ができます。
指定するオブジェクトの雛形は使い回すことができます。

単独のオブジェクトの雛形を設置する以外に、複数の雛形のセットを指定することもできます。その際は、リストの長さがおなじになるようにユーザー側で責任を持って指定する必要があります。

pybullet における座標系は(見た限りおそらく)右手系になります。
回転は四元数での指定となります。四元数については、次のQiitaの記事がとてもわかりやすいです。
https://qiita.com/kenjihiranabe/items/945232fbde58fab45681

また、質量を 0 にすると、動かない固定の物体として解釈されるようです。

単独雛形のみの場合

mass = 5.0 # kg
position = [0, 1, 0]
orientation = [1, 0, 0, 0] # 四元数
sphere_bid = pybullet.createMultiBody(mass, sphere_cid, sphere_vid, position, orientation, physicsClientId=engine_id)

複数雛形を組み合わせる場合

4.3.3 物理モデルの確認

pybullet.GUI でGUIを立ち上げた場合、設置したオブジェクトはGUIのウインドウ上で確認できます。(試していないですが、多分)

一方、pybullet.DIRECTでGUI無しで物理エンジンを作成した場合も、 pybullet.getCameraImage 関数で構築した物理モデルを描画することができます。

上述の公式のページには関数名だけで、APIの詳細が記載されていないので困りましたが、次のQiita上の記事を参考にさせてもらいました。
https://qiita.com/saketosakana/items/7e333cc382b939e6bd86

getCameraImageの引数

help(pybullet.getCameraImage) でもう少しオプションの情報が出てきますが、これだけ読んでもあんまり分かりませんでした。おそらく、カメラ位置やカメラ方向に関するオプションが有るのだとは思うのですが。

help(pybullet.getCameraImage)
Help on built-in function getCameraImage in module pybullet:

getCameraImage(...)
    Render an image (given the pixel resolution width, height, camera viewMatrix , projectionMatrix, lightDirection, lightColor, lightDistance, shadow, lightAmbientCoeff, lightDiffuseCoeff, lightSpecularCoeff, and renderer), and return the 8-8-8bit RGB pixel data and floating point depth values as NumPy arrays
from PIL import Image

width = 1024
height = 768

# 返り値は、tuple型: (width, height, rgbPixels, depthPixels, segmentationMaskBuffer)
img = pybullet.getCameraImage(width, height, physicsClientId=engine_id)

# 画像が返却されるので、Jupyte Notebook や Google Colabのセルの最終行に記載しておくと画像が表示される。
# 原点 (0,0,0) からX軸正方向 (+1,0,0) に向かっての画像だと思われる。
Image.fromarray(img[2])

4.3.4 物理モデルの保存

pybullet.saveWorld関数で作成した物理モデルをBullet形式のファイルに保存できます。

pybullet.saveWorld("mymodel.bullet", engine_id)

4.4 シミュレーション

4.4.1 シミュレーションを進める

pybullet.stepSimulation 関数によりシミュレーションを1ステップすすめることができます。

pybullet.stepSimulation(engine_id)

4.4.2 オブジェクトの状態の確認

設置したオブジェクトの位置と方向は pybullet.getBasePositionAndOrientation 関数で取得できます。

pos, ori = pybullet.getBasePositionAndOrientation(sphere_bid, physicsClientId=engine_id)

4.4.3 オブジェクトの操作

ジョイントのモーターの操作は、pybullet.setJointMotorControl2 または、 pybullet.setJointMotorControlArray を利用します。

5. 感想

PyBulletを使ってみた系の記事はいずれも出来合いのオブジェクトのファイルを読み込んで利用する形式だったので、PyBullet内でオブジェクトを構築する方法を調査しましたが難航しました。

様々なオブジェクトをただのint型のIDで返してきて、ユーザーが覚えておく必要があるのは大変に感じます。

また、グローバルな関数を利用して操作をしますが、同じ関数でも操作するオブジェクトの種類によって必要なオプションが変わるのもしんどいですね。

できるだけ早く続きを公開しますので、乞うご期待。

追記 (2021/9/22)

PyBulletを強化学習用に使いやすくしたFacebook Research製の wrapper ライブラリ pybulletX について記事を書きました。
https://zenn.dev/ymd_h/articles/e430f3459b7186

追記 (2021/9/26)

いつのまにか、 issueDiscussions にアクセスできるようになっていました。
(READMEには閉じたままだとの文言が残っていますが。)

追記 (2021/9/26)

次の記事を公開しました。
https://zenn.dev/ymd_h/articles/381fa7cc4070e8

脚注
  1. X11 (X Window System) が利用できない環境だとエラーになります。 ↩︎

  2. Bulletの独自フォーマットだと思いますが、レファレンス見つけられませんでした。 ↩︎

Discussion