ゆるふわフィジカルAI
パソコンで作業中のゆるロボ

MuJoCoで逆運動学を使う|IKで目標位置へ手先を届かせる動かし方

2026-11-02 ・ 実践

#MuJoCo#逆運動学#ロボットアーム#実践

MuJoCoで逆運動学(IK)を使うとは、「手先をこの位置に置きたい」という目標座標から関節角度を逆算し、その角度をPID制御の目標値として物理エンジン内のアームに与えることです。 MuJoCoでロボットアームを制御するでは「肩45度・肘-30度」のように目標角度を人が直接決めていましたが、実際のタスクで欲しいのはたいてい「目標の手先位置」です。この記事では逆運動学をPythonで実装するで作ったIKの計算を物理エンジンの中のアームにつなぎ、本当に目標位置へ届くかをシミュレーションで確かめます。

IKで目標位置に届かせる流れ

🎯

目標位置を決める

手先のx, z座標

→
🧮

IKで角度を逆算

shoulder, elbowの目標値

→
🎮

PIDで動かす

mujoco-robot-arm-controlと同じ制御

なぜ「目標角度」ではなく「目標位置」で考えたいのか

ロボットに頼みたいことは、ふつう「肩を何度にして」ではなく「ここにあるネジの位置に手先を持っていって」です。順運動学は関節角度から手先位置を求める一意な計算でしたが、欲しいのはその逆——**手先位置から関節角度を求める逆運動学(IK)**です。逆運動学をPythonで実装するでは2関節アームの幾何学的な解法をnumpyだけで確認しました。ここではその解をMuJoCoでロボットアームを制御するのPIDコントローラに渡し、実際に物理エンジンの中で目標位置まで動くかを検証します。

IKの関数を書く

2関節アーム(上腕0.35m・前腕0.3m)はMuJoCoでロボットアームを制御すると同じMJCFを使います。手先の位置を読み取れるように <site name="hand"> を前腕の先端に追加しておきます。

arm_ik.py
Python
import mujoco
import numpy as np
 
XML = """
<mujoco>
  <option gravity="0 0 -9.81"/>
  <worldbody>
    <light pos="0 0 3"/>
    <geom type="plane" size="2 2 0.1" rgba="0.9 0.9 0.95 1"/>
    <body name="upper_arm" pos="0 0 1.0">
      <joint name="shoulder" type="hinge" axis="0 1 0"/>
      <geom type="capsule" fromto="0 0 0  0.35 0 0" size="0.03" mass="1.0"/>
      <body name="forearm" pos="0.35 0 0">
        <joint name="elbow" type="hinge" axis="0 1 0"/>
        <geom type="capsule" fromto="0 0 0  0.3 0 0" size="0.025" mass="0.6"/>
        <site name="hand" pos="0.3 0 0" size="0.02"/>
      </body>
    </body>
  </worldbody>
  <actuator>
    <motor name="shoulder_motor" joint="shoulder" ctrlrange="-20 20"/>
    <motor name="elbow_motor" joint="elbow" ctrlrange="-20 20"/>
  </actuator>
</mujoco>
"""
 
L1, L2 = 0.35, 0.3  # mujoco-robot-arm-control.mdxと同じ上腕・前腕の長さ
 
 
def inverse_kinematics(x, z, elbow_up=True):
    """手先座標(x, z)から関節角度(shoulder, elbow)を逆算する。届かない場合は腕を伸ばした姿勢に丸める"""
    y = -z  # MuJoCoのz軸は、2関節IKの教科書的なy軸と符号が逆(後述)
    cos_e = (x * x + y * y - L1 * L1 - L2 * L2) / (2 * L1 * L2)
    reachable = -1.0 <= cos_e <= 1.0
    cos_e = np.clip(cos_e, -1.0, 1.0)
    elbow = np.arccos(cos_e)
    if elbow_up:
        elbow = -elbow
    k1 = L1 + L2 * np.cos(elbow)
    k2 = L2 * np.sin(elbow)
    shoulder = np.arctan2(y, x) - np.arctan2(k2, k1)
    return shoulder, elbow, reachable

inverse_kinematicsが返すreachableがFalseのときは、腕をまっすぐ伸ばした(または曲げた)境界の姿勢に角度が丸められます。目標に届かないことを知らせるフラグとして使います。

同じ位置でも解は2つある:肘上げと肘下げ

逆運動学をPythonで実装するで触れたとおり、同じ手先位置に対して肘を上げる解・下げる解の2通りが存在します。elbow_upの真偽を変えるだけで、MuJoCo上でも両方の解が同じ座標に収束することを確認します。

📐

A: 肘下げ(elbow_up=False)

  • 肘の角度は正方向に大きく曲がる
  • shoulderはほぼ水平に近い角度
  • 狭い場所の下から回り込む動きに近い
VS
📏

B: 肘上げ(elbow_up=True)

  • 肘の角度は負方向に大きく曲がる
  • shoulderは大きく持ち上がる
  • 上から回り込む動きに近い

MuJoCoでロボットアームを制御するで使ったPIDコントローラに、IKが返した角度を目標値として渡すだけです。

arm_ik.py(続き)
Python
class PID:
    def __init__(self, kp, ki, kd, dt):
        self.kp, self.ki, self.kd, self.dt = kp, ki, kd, dt
        self.integral = 0.0
        self.prev_error = 0.0
 
    def update(self, target, current):
        error = target - current
        self.integral += error * self.dt
        derivative = (error - self.prev_error) / self.dt
        self.prev_error = error
        return self.kp * error + self.ki * self.integral + self.kd * derivative
 
 
def run_to_target(x, z, elbow_up, steps=5000):
    model = mujoco.MjModel.from_xml_string(XML)
    data = mujoco.MjData(model)
    hand_site = model.site("hand").id
    shoulder_origin = np.array([0, 0, 1.0])
 
    target_shoulder, target_elbow, reachable = inverse_kinematics(x, z, elbow_up)
    dt = model.opt.timestep
    shoulder_pid = PID(kp=60.0, ki=30.0, kd=8.0, dt=dt)
    elbow_pid = PID(kp=40.0, ki=20.0, kd=5.0, dt=dt)
 
    for _ in range(steps):
        data.ctrl[0] = shoulder_pid.update(target_shoulder, data.qpos[0])
        data.ctrl[1] = elbow_pid.update(target_elbow, data.qpos[1])
        mujoco.mj_step(model, data)
 
    hand = data.site_xpos[hand_site] - shoulder_origin
    hand_xz = (hand[0], hand[2])  # 平面運動なのでy成分(hand[1])は常に0
    err_mm = np.hypot(hand_xz[0] - x, hand_xz[1] - z) * 1000
    return reachable, hand_xz, err_mm
 
 
print("A(肘下げ):", run_to_target(0.45, -0.25, elbow_up=False))
print("B(肘上げ):", run_to_target(0.45, -0.25, elbow_up=True))
A(肘下げ): (True, (0.450, -0.250), 0.3)
B(肘上げ): (True, (0.450, -0.250), 0.4)

肩・肘の角度はまったく違う(A: shoulder=-5.3°, elbow=75.5° / B: shoulder=63.4°, elbow=-75.5°)のに、手先は同じ(0.45, -0.25)に1mm未満の誤差で収束しています。

📍

2通り

同じ手先位置に至る関節角度

🎯

0.3〜0.4mm

5000ステップ後の収束誤差

届かない位置を指定するとどうなるか

腕の長さは L1+L2 = 0.65m です。これより遠い (0.9, 0) を目標にすると、inverse_kinematicsはreachable=Falseを返し、角度は腕をまっすぐ伸ばした姿勢に丸められます。

arm_ik.py(続き)
Python
print("C(届かない目標):", run_to_target(0.9, 0.0, elbow_up=False))
C(届かない目標): (False, (0.650, -0.000), 250.0)

目標との誤差はちょうど250mm——これは「目標距離0.9m」と「腕の長さ0.65m」の差(0.25m)と一致します。つまりPID制御そのものは正しく動いていて、そもそも届かない場所をIKに頼んでいたことが分かります。

⚠️

reachableを見ずに使うと気づきにくい

reachableを確認せずに角度だけ使うと、プログラムはエラーを出さずに「腕を伸ばした姿勢」で普通に収束します。見た目は正常に動いているので、目標に届いていないことに気づきにくい落とし穴です。実機で使う場合は、reachable=Falseのときはタスクを中断するか、安全な代替姿勢に戻す処理を入れてください。

つまずきやすい点

  • z座標の符号を逆にすると、まったく違う場所に動く → MuJoCoのこのモデルはz軸が上方向で、肩関節の回転軸はaxis="0 1 0"(y軸まわり)です。角度がゼロのとき腕は+x方向を向き、そこから回転するとxはcosθ、zは-sinθで変化します。逆運動学をPythonで実装するの公式は「角度ゼロでx軸、そこから回転するとyがsinθで増える」教科書的な2次元平面を想定しているため、zをそのまま使うと符号が反転した位置に収束します。本記事のコードはy = -zと変換してから公式に渡すことでこれを回避しています
  • 手先座標をhand[1]と間違える → data.site_xposは[x, y, z]の3要素です。この記事のアームはx-z平面でしか動かないので、使うべきはhand[0](x)とhand[2](z)です。hand[1](常に0のy成分)を使うと、どんな目標を入れても手先が同じ場所に見えてしまいます
  • 振動して収束しない → MuJoCoでロボットアームを制御すると同様、kpが大きすぎるかもしれません。まず半分にしてみてください

まとめ

  • IKで求めた関節角度を、既存のPIDコントローラにそのまま目標値として渡せば「目標位置へ届く」動きになる
  • 同じ手先位置でも肘上げ・肘下げの2つの解があり、どちらも同じ座標に収束する(角度はまったく違う)
  • 腕の長さを超える目標はreachable=Falseになり、角度は境界姿勢に丸められる。これを確認しないと「届いていない」ことに気づきにくい
  • MuJoCoの座標系とIKの教科書的な座標系は軸の符号が異なる場合があるため、変換を1箇所にまとめておくと事故が減る

よくある質問

Q. このIKの計算はMuJoCoの標準機能ですか?

A. いいえ、自作した幾何学的な式です。MuJoCoには関節角度と手先速度の関係(ヤコビ行列)を返すmj_jac系のAPIもあり、関節数が増えて幾何学的に解けない場合は、そちらを使う数値的な解法(逆運動学をPythonで実装するで紹介したヤコビ行列法)に切り替えるのが実務的です。

Q. elbow_up=Trueとelbow_up=Falseはどちらを使えばよいですか?

A. どちらを選んでも手先の位置は同じになるため、周囲の障害物を避けやすい姿勢や、関節の可動範囲に収まる方を選びます。実機では配線やモーターの可動範囲の都合で、片方の解しか使えないこともあります。

Q. 3関節以上のアームでも同じ式でIKを解けますか?

A. いいえ、この記事の式は2関節アーム専用の閉じた式です。関節が増えると幾何学的に式を作るのが難しくなるため、逆運動学をPythonで実装するで紹介したヤコビ行列を使う数値的な解法に切り替える必要があります。

📚 もう少し詳しく(解析的IKと数値的IKの境目)気になる人向け・タップで開く

この記事で使った2関節アームの解法は、三角関数と余弦定理だけで閉じた式(解析解)が書ける特別なケースです1。関節が増えたり、3次元で姿勢(向き)まで指定したりすると、一般には閉じた式を作れなくなり、逆運動学をPythonで実装するで紹介したヤコビ行列の擬似逆行列を使う数値的な方法が使われます。

MuJoCoでロボットの制御を作るとき、IKの計算自体は物理エンジンの外(numpyなど)で行い、その結果の関節角度だけをPIDやpositionアクチュエータに渡す、という役割分担が一般的です。物理エンジンは「角度や力を与えたらどう動くか」を計算する担当で、「どの角度にすればよいか」を決めるのはその外側のロジックという切り分けです。

次の一歩 🌸

IKの基礎(複数解・解なしの扱い)をもう一度確認したい場合は逆運動学をPythonで実装する、PID制御の仕組み自体はPID制御の実装、ロボットの構造の記述形式はURDFとはへどうぞ。実機のアームで同じことを試したくなったらSO-101組み立てガイドを参考にしてください。

Footnotes

  1. 2関節(平面)アームは、余弦定理から肘の角度を直接計算できるため、反復計算なしの閉じた式でIKが解ける。関節数が増えると一般に閉じた式は作れなくなり、ヤコビ行列を用いた反復的な数値解法が使われる。 ↩

この記事をシェア:𝕏 で共有LINEで送る

🔥 この分野の最新トレンドをチェック →

あわせて読みたい