1. はじめに:絵に描いた餅を、食べられる餅にする
前回の [Day 3] では、Fusion 360を使ってパラメトリック設計による「100mmキューブ・サンドイッチ構造」の筐体を作り上げました。
しかし、これはまだPCの中にあるただの**「絵(3Dモデル)」**に過ぎません。重さもなければ、摩擦もありません。このままでは、いくらコードを書いても何も動きません。
本日のゴールは、この設計データを物理シミュレータ (PyBullet) の中に 「デジタルツイン(物理的実体を持った分身)」 として召喚することです。
2. 戦略:プラグインを使わない「手組みURDF」
Fusion 360からロボット定義ファイル (URDF) を自動で書き出すプラグインはいくつか存在します。
しかし、今回はあえてそれらを使いません。
なぜ手書きなのか?
自動変換ツールは便利ですが、座標変換がブラックボックスになりがちです。
「シミュレータ上では動くのに、実機だと逆に動く」「なぜか空中に固定される」といったバグに遭遇したとき、中身を理解していないと詰みます。
今回の手法
- Fusion 360からパーツごとの形状データ (STL) を書き出す。
- XML形式の URDFファイル を自分の手で書き、構造(関節の位置、質量の中心など)を定義する。
この「泥臭い」アプローチこそが、Sim2Real(実機への転移)を成功させるための急がば回れルートです。
3. 実装:STLエクスポートとURDFの記述
3.1 STLの準備
Fusion 360から、動かないベース部分 (body.stl) と、回転するフライホイール部分 (flywheel.stl) をそれぞれ書き出します。
3.2 URDFの魔術(ここがハマりポイント)
ロボットの構成図である my_cubli.urdf を作成します。
URDFでは、剛体を表す <link> と、関節を表す <joint> を組み合わせて記述します。
ここで最も重要、かつハマりやすいのが 「見た目 (Visual)」 と 「物理 (Inertial)」 の分離 です。
直面した課題:物理エンジンの発狂
最初、Fusion 360の設計座標をそのままURDFに入力しました。
するとシミュレータを起動した瞬間、フライホイールが筐体の外にテレポートし、地面に激突してガタガタと荒ぶる現象(通称:物理エンジンの発狂)が発生しました。
原因は、STLファイルの原点と、物理演算上の重心位置(Center of Mass)の定義ズレです。
解決策:重心オフセット
試行錯誤の結果、見た目の位置合わせはそのままで、物理的な重心定義だけをずらすことで解決しました。
<?xml version="1.0"?>
<robot name="my_cubli">
<material name="white">
<color rgba="0.9 0.9 0.9 1"/>
</material>
<material name="gold">
<color rgba="0.8 0.6 0.2 1"/>
</material>
<link name="base_link">
<visual>
<geometry>
<mesh filename="meshes/body.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="white"/>
</visual>
<collision>
<geometry>
<mesh filename="meshes/body.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
<inertial>
<origin xyz="0.05 0.05 0.05" rpy="0 0 0"/>
<mass value="0.4"/>
<inertia ixx="0.0006" ixy="0" ixz="0" iyy="0.0006" iyz="0" izz="0.0006"/>
</inertial>
</link>
<link name="wheel_x">
<visual>
<geometry>
<mesh filename="meshes/flywheel.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="gold"/>
</visual>
<inertial>
<origin xyz="0 0 -0.05" rpy="0 0 0"/>
<mass value="0.1"/>
<inertia ixx="0.0001" ixy="0" ixz="0" iyy="0.0001" iyz="0" izz="0.0001"/>
</inertial>
</link>
<joint name="joint_x" type="continuous">
<parent link="base_link"/>
<child link="wheel_x"/>
<origin xyz="0.0 0.045 0.045" rpy="0 1.5708 0"/>
<axis xyz="0 0 1"/>
</joint>
<link name="wheel_y">
<visual>
<geometry>
<mesh filename="meshes/flywheel.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="gold"/>
</visual>
<inertial>
<origin xyz="0 0 -0.05" rpy="0 0 0"/>
<mass value="0.1"/>
<inertia ixx="0.0001" ixy="0" ixz="0" iyy="0.0001" iyz="0" izz="0.0001"/>
</inertial>
</link>
<joint name="joint_y" type="continuous">
<parent link="base_link"/>
<child link="wheel_y"/>
<origin xyz="0.045 0.09 0.045" rpy="1.5708 0 0"/>
<axis xyz="0 0 1"/>
</joint>
<link name="wheel_z">
<visual>
<geometry>
<mesh filename="meshes/flywheel.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="gold"/>
</visual>
<inertial>
<origin xyz="0 0 -0.05" rpy="0 0 0"/>
<mass value="0.1"/>
<inertia ixx="0.0001" ixy="0" ixz="0" iyy="0.0001" iyz="0" izz="0.0001"/>
</inertial>
</link>
<joint name="joint_z" type="continuous">
<parent link="base_link"/>
<child link="wheel_z"/>
<origin xyz="0.045 0.045 -0.0" rpy="0 0 0"/>
<axis xyz="0 0 1"/>
</joint>
</robot>
この 「見た目は変えず、重心だけ操る」 テクニックにより、物理的に安定したモデルを作ることができました。
4. 召喚の儀(デモ動画)
準備は整いました。
Fusion 360から書き出し、手書きで修正したURDFを読み込むPythonスクリプト (sim_cubli.py) を作成します。
ここでは、単に表示するだけでなく、「フライホイールにトルク(回転力)を与えて、筐体がどう反応するか」 をテストします。
実装コード: sim_cubli.py
import pybullet as p
import pybullet_data
import time
import math
# GUI起動
try:
physicsClient = p.connect(p.GUI)
except Exception as e:
print("Failed to connect to PyBullet GUI:", e)
exit()
p.setAdditionalSearchPath(pybullet_data.getDataPath())
p.setGravity(0, 0, -9.81)
planeId = p.loadURDF("plane.urdf")
# カメラ設定 (接写)
p.resetDebugVisualizerCamera(
cameraDistance=0.2,
cameraYaw=0,
cameraPitch=-30,
cameraTargetPosition=[0,0,0.05]
)
boxId = None
startPos = [0, 0, 0.1]
angle_45deg = math.pi / 4
# 少し傾けた状態で出現させる
startOrientation = p.getQuaternionFromEuler([angle_45deg*3, angle_45deg, angle_45deg])
# ★ フラグ管理
is_paused = False
is_motor_on = False # モーター回転フラグ
print("------------------------------------------------")
print(" [a] 出現 / リセット")
print(" [p] 一時停止 / 再開")
print(" [m] モーター回転 ON/OFF (Motor)")
print(" [SPACE] ジャンプ")
print("------------------------------------------------")
while True:
# キーボード入力の取得
keys = p.getKeyboardEvents()
# ------------------------------------------------
# ★ 'p' キーで一時停止切り替え
# ------------------------------------------------
if ord('p') in keys and keys[ord('p')] & p.KEY_WAS_TRIGGERED:
is_paused = not is_paused
print("Paused!" if is_paused else "Resumed!")
# ------------------------------------------------
# ★ 'm' キーでモーター回転 ON/OFF 切り替え
# ------------------------------------------------
if ord('m') in keys and keys[ord('m')] & p.KEY_WAS_TRIGGERED:
is_motor_on = not is_motor_on
state_str = "ON (Spinning!)" if is_motor_on else "OFF (Coasting)"
print(f"Motor Torque: {state_str}")
# ------------------------------------------------
# ★ 'a' キーで出現
# ------------------------------------------------
if ord('a') in keys and keys[ord('a')] & p.KEY_WAS_TRIGGERED:
if boxId is not None:
p.removeBody(boxId)
# ロボット読み込み
boxId = p.loadURDF("my_cubli.urdf", startPos, startOrientation, globalScaling=1.0)
# 重心の可視化
p.setDebugObjectColor(boxId, -1, objectDebugColorRGB=[1,1,0])
# ブレーキ解除(速度制御を無効化)
p.setJointMotorControl2(boxId, 0, p.VELOCITY_CONTROL, force=0)
p.setJointMotorControl2(boxId, 1, p.VELOCITY_CONTROL, force=0)
p.setJointMotorControl2(boxId, 2, p.VELOCITY_CONTROL, force=0)
# リセット時はモーターOFFに戻す
is_motor_on = False
print("Spawned!")
# ------------------------------------------------
# ★ スペースキーでジャンプ
# ------------------------------------------------
if 32 in keys and keys[32] & p.KEY_WAS_TRIGGERED:
if boxId is not None:
print("Jump!")
p.applyExternalForce(boxId, -1, forceObj=[150, 150, 150], posObj=[0, 0, 0], flags=p.LINK_FRAME)
p.applyExternalTorque(boxId, -1, torqueObj=[5, 5, 0.1], flags=p.LINK_FRAME)
# ------------------------------------------------
# ★ シミュレーションステップ
# ------------------------------------------------
if not is_paused:
if boxId is not None:
# フラグに応じてトルクを切り替え
# ONなら 0.15Nm, OFFなら 0Nm
target_torque = 0.15 if is_motor_on else 0.0
p.setJointMotorControl2(boxId, 0, p.TORQUE_CONTROL, force=target_torque) # X軸
p.setJointMotorControl2(boxId, 1, p.TORQUE_CONTROL, force=target_torque) # Y軸
p.setJointMotorControl2(boxId, 2, p.TORQUE_CONTROL, force=target_torque) # Z軸
p.stepSimulation()
# GUI更新ウェイト
time.sleep(1./240.)
p.disconnect()
実行結果
このスクリプトを実行すると、デジタルツインが召喚されます。
- 出現: 何もない空間で Enter キーを押すと、上空からCubliが「ストン」と落ちてきます。
- 静止: 荒ぶることなくピタリと着地しました。
- 回転: mキーを押してモーターにトルク指令を送ると、金色のホイールがギュイーンと回り出します。
成功の証:反作用トルク
動画をよーく見てください。
ホイールが回転し始めた瞬間、白いボディが逆方向(ホイールと反対向き)に回転しようとしているのが分かりますか?
これぞ 「作用・反作用の法則」。
物理エンジンが正しく機能し、回転トルクがボディに伝わっている証拠です。この反動を利用して姿勢を制御するのが、倒立振子の原理そのものです。
5. まとめと次回予告
本日の成果
- Fusion 360のデザインデータを、物理法則を持つロボットとしてPyBullet内に召喚しました。
- STLの座標ズレと重心問題を解決し、安定したシミュレーション環境が整いました。
次回予告 [Day 5]
器(ハードウェア)は完成しました。次は魂(ソフト)を入れます。
いよいよ 「制御理論」 の出番です。
明日は、このロボットを、 PID制御コード でピタリと直立させてみせます!
アドベントカレンダー参加中
STM32×AIで「3軸倒立振子」を作る25日間(ひとりアドカレ)Advent Calendar 2025
