Unitree G1のDex3ハンドをIsaac Sim上で動かす

Unitree G1のDex3ハンドをIsaac Sim上で動かす

2026.04.09

はじめに

前回の記事ではIsaac Lab + Unitree G1のシミュレーション環境を構築し、シーン上にG1ロボットを表示するところまで確認しました。本記事では、DDSを用いてG1のDex3ハンド(3本指ハンド)をシミュレーション上で動かします。

Dex3の各関節のマッピングや、左右の手で回転方向が異なるという仕様上の注意点についても解説します。

前回の記事
https://dev.classmethod.jp/articles/isaac-lab-unitree-g1-simulation-setup/

DDSとは

DDS(Data Distribution Service)は、リアルタイムのデータ配信に特化した通信プロトコルです。unitree_sim_isaaclabはUnitree実機と同じDDSを採用しています。これにより、シミュレーション上で動かしたコードを最小限の変更で実機にも適用できるというメリットがあります。

Unitree SDKでは内部的にCycloneDDSを使用しています。

前提

  • 前回の記事の環境構築が完了していること

DDS通信の設定ポイント

Isaac Sim側のDDSの設定を確認しておきます。dds/dds_master.pyを見ると、以下のように初期化されています。

ChannelFactoryInitialize(1)  # ドメインID=1、インターフェース自動選択

外部スクリプトからコマンドを送る際は、同じドメインID(1)を指定する必要があります。ここを間違えるとコマンドが届きません。

最初にテストした際、以下のように書いてしまい通信できないという問題に遭遇しました。

# NG: ドメインIDが0、インターフェースがloopback
ChannelFactoryInitialize(0, 'lo')

# OK: Isaac Sim側と同じドメインID
ChannelFactoryInitialize(1)

Dex3ハンドの制御トピックは以下の通りです。

  • 右手: rt/dex3/right/cmd
  • 左手: rt/dex3/left/cmd

ハンドコマンドの構造

Unitree SDK2 PythonのDex3制御に使うデータ型はHandCmd_MotorCmd_です。(後のセクションのデモで出てきます)

HandCmd_の初期化には引数が必要で、デフォルトコンストラクタは使えません。

from unitree_sdk2py.idl.unitree_hg.msg.dds_ import HandCmd_, MotorCmd_

# NG: 引数なしではエラーになる
cmd = HandCmd_()

# OK: motor_cmdとreserveを明示的に渡す
motors = [
    MotorCmd_(mode=1, q=0.5, dq=0.0, tau=0.0, kp=10.0, kd=1.0, reserve=0)
    for _ in range(7)
]
cmd = HandCmd_(motor_cmd=motors, reserve=[0, 0, 0, 0])

MotorCmd_の各パラメータの意味は以下の通りです。

パラメータ 説明
mode uint8 制御モード(1=位置制御)
q float32 目標関節角度
dq float32 目標関節角速度
tau float32 トルク指令
kp float32 位置制御ゲイン(P制御)
kd float32 速度制御ゲイン(D制御)
reserve uint32 予約領域

Dex3の関節マッピング

Dex3は片手あたり7自由度で、motor_cmdの7要素が以下の関節に対応しています。

インデックス 関節名
0 thumb_0_joint 親指の根元
1 thumb_1_joint 親指の中間
2 thumb_2_joint 親指の先端
3 middle_0_joint 中指の根元
4 middle_1_joint 中指の先端
5 index_0_joint 人差し指の根元
6 index_1_joint 人差し指の先端

このマッピングはaction_provider/action_provider_dds.pyleft_hand_joint_mappingおよびright_hand_joint_mappingで定義されています。

左右の手で閉じる方向が異なる

実際にさまざまな値を送って動作を確認したところ、左手と右手の両方で、関節ごとに閉じる方向(符号)が異なることがわかりました。どちらの手も一律に同じ符号を送るだけでは正しく閉じません。

関節 右手(閉じる) 左手(閉じる)
thumb_0 (親指の根元) +1.0 +1.0
thumb_1 (親指の中間) +1.0 -1.0
thumb_2 (親指の先端) -1.0 +1.0
middle_0 (中指の根元) +1.0 -1.0
middle_1 (中指の先端) +1.0 -1.0
index_0 (人差し指の根元) +1.0 -1.0
index_1 (人差し指の先端) +1.0 -1.0

つまり、両手を同時に閉じるには以下のように関節ごとに符号を指定する必要があります。

# 右手を閉じる: thumb_2だけ-、他は+
right_cmd = make_cmd([1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0])

# 左手を閉じる: thumb_0とthumb_2だけ+、他は-
left_cmd = make_cmd([1.0, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0])

G1のDex3ハンドは左右で鏡像配置になっており、関節ごとに回転軸の構造が異なります。特にthumb_2(親指の先端)は左右で符号が逆転しています。対応表を参照して関節ごとに個別に符号を設定する必要があります。

シミュレーションの起動

まずIsaac Simでシミュレーションを起動します。

conda activate unitree_sim_env
cd ~/unitree_sim_isaaclab
python sim_main.py --device cpu --enable_cameras --task Isaac-PickPlace-RedBlock-G129-Dex3-Joint --enable_dex3_dds --robot_type g129

タスクにはRedBlockを使用しています。前回の記事で使用したCylinderタスクではオブジェクトがG1の手元に近い位置に配置されており、手の動きを確認する際に干渉するためです。RedBlockタスクではオブジェクトがロボットから離れた位置に配置されるため、ハンドの動作確認に適しています。

起動時のデフォルトカメラアングルだと手元が全く見えません。
ビューポート(ロボットやシーンが写っているエリア)の上部にカメラアイコンがあるので、そこを選択して「Cameras > front_cam」とすると、ロボットの目線で手元を見ることができます。

カメラ設定箇所
スクリーンショット 2026-04-09 13.54.15

front_cam
スクリーンショット 2026-04-09 13.35.18

もしくは「Perspective」を選択して、手動で見やすいアングルに設定することもできます。
screen

マウス右クリックで視点の回転、ホイールクリックで移動、ホイール回転でズームができます。

補足: シーンの照明をカスタムする

デフォルトのRedBlockシーンはライト設定がコメントアウトされており、倉庫モデル自体の照明のみで暗めです。
ハンドの色が黒いため、影の影響で指の曲がり具合などが視認しづらく、左右の手が同じ状態になっているかを判別するのが困難です。

スクリーンショット 2026-04-09 13.34.56
スクリーンショット 2026-04-09 13.35.18

手元が見にくい場合は、シーン設定ファイルにライトを追加すると確認しやすくなります。
スクリーンショット 2026-04-09 13.30.48

tasks/common_scene/base_scene_pickplace_redblock.py内のコメントアウトされたライト設定を以下のように書き換えます。

    # 4. light configuration
    light = AssetBaseCfg(
        prim_path="/World/light",
        spawn=sim_utils.DomeLightCfg(color=(0.9, 0.9, 0.9), intensity=4000.0),
    )
    light_top = AssetBaseCfg(
        prim_path="/World/light_top",
        spawn=sim_utils.DistantLightCfg(color=(1.0, 1.0, 1.0), intensity=2000.0),
    )

DomeLightCfgはシーン全体を均一に照らす環境光、DistantLightCfgは太陽光のような平行光源です。intensityの値を上げると明るくなります。変更後はIsaac Simの再起動が必要です。

と、この記事を書いていて気がついたのですが、「Camera Light」を選択すれば、全体的に良い感じの明るさになりますね。
ただ質感的なところは結構マットな印象なので、この辺りは見え方の好みで、上のカスタムライトの使用なども検討いただければと思います。

スクリーンショット 2026-04-09 13.37.48

開閉テスト

シーンが表示されたら、別ターミナルを開き、以下のコマンドでスクリプトをファイルに保存します。

cat > ~/unitree_sim_isaaclab/test_dex3.py << 'EOF'
import time
from unitree_sdk2py.core.channel import ChannelPublisher, ChannelFactoryInitialize
from unitree_sdk2py.idl.unitree_hg.msg.dds_ import HandCmd_, MotorCmd_

ChannelFactoryInitialize(1)

pub_right = ChannelPublisher('rt/dex3/right/cmd', HandCmd_)
pub_right.Init()
pub_left = ChannelPublisher('rt/dex3/left/cmd', HandCmd_)
pub_left.Init()

def make_cmd(q_values):
    motors = [
        MotorCmd_(mode=1, q=q_values[i], dq=0.0, tau=0.0, kp=10.0, kd=1.0, reserve=0)
        for i in range(7)
    ]
    return HandCmd_(motor_cmd=motors, reserve=[0, 0, 0, 0])

CLOSE = 1.0
# 右手: thumb_2だけ-、他は+
RIGHT_CLOSE = [CLOSE, CLOSE, -CLOSE, CLOSE, CLOSE, CLOSE, CLOSE]
# 左手: thumb_0とthumb_2だけ+、他は-
LEFT_CLOSE = [CLOSE, -CLOSE, CLOSE, -CLOSE, -CLOSE, -CLOSE, -CLOSE]

print('両手を閉じます...')
for i in range(300):
    pub_right.Write(make_cmd(RIGHT_CLOSE))    # 右手: 関節ごとに符号が異なる
    pub_left.Write(make_cmd(LEFT_CLOSE))      # 左手: 関節ごとに符号が異なる
    time.sleep(0.02)

print('両手を開きます...')
for i in range(300):
    pub_right.Write(make_cmd([0.0] * 7))
    pub_left.Write(make_cmd([0.0] * 7))
    time.sleep(0.02)

print('完了')
EOF

保存したスクリプトを実行します。

cd ~/unitree_sim_isaaclab
conda activate unitree_sim_env 
python test_dex3.py

Isaac SimのビューポートでG1の手元を見ると、両手が閉じてから開く動きが確認できます。

https://youtu.be/SRMUEq1jSHg?si=fO-HSVsnHaTTJiMJ

インタラクティブコントローラーの作成

テスト用の1回だけ実行するスクリプトだけでなく、対話的にハンドを操作できるコントローラーも作成しました。

ポイントは以下の通りです。

  • バックグラウンドスレッドで20ms間隔(50Hz)で常にコマンドを送信し続ける
  • メインスレッドでユーザーのコマンド入力を受け付ける
  • smooth_move関数で目標値を段階的に変化させ、なめらかな動作を実現する
  • 左右の手で回転方向が逆であることを吸収し、close/openコマンドで直感的に操作できるようにする
cat > ~/unitree_sim_isaaclab/dex_controller.py << 'EOF'
import time
import threading
from unitree_sdk2py.core.channel import ChannelPublisher, ChannelFactoryInitialize
from unitree_sdk2py.idl.unitree_hg.msg.dds_ import HandCmd_, MotorCmd_

ChannelFactoryInitialize(1)

pub_right = ChannelPublisher('rt/dex3/right/cmd', HandCmd_)
pub_right.Init()
pub_left = ChannelPublisher('rt/dex3/left/cmd', HandCmd_)
pub_left.Init()

# 閉じる方向は関節ごとに異なる
left_q = [0.0] * 7
right_q = [0.0] * 7
kp = 10.0
kd = 1.0
running = True
CLOSE_VAL = 1.0
# 右手: thumb_2だけ-、他は+
RIGHT_CLOSE = [CLOSE_VAL, CLOSE_VAL, -CLOSE_VAL, CLOSE_VAL, CLOSE_VAL, CLOSE_VAL, CLOSE_VAL]
# 左手: thumb_0とthumb_2だけ+、他は-
LEFT_CLOSE = [CLOSE_VAL, -CLOSE_VAL, CLOSE_VAL, -CLOSE_VAL, -CLOSE_VAL, -CLOSE_VAL, -CLOSE_VAL]

def make_cmd(q_values):
    motors = [
        MotorCmd_(mode=1, q=q_values[i], dq=0.0, tau=0.0, kp=kp, kd=kd, reserve=0)
        for i in range(7)
    ]
    return HandCmd_(motor_cmd=motors, reserve=[0, 0, 0, 0])

def publish_loop():
    while running:
        pub_left.Write(make_cmd(left_q))
        pub_right.Write(make_cmd(right_q))
        time.sleep(0.02)

t = threading.Thread(target=publish_loop, daemon=True)
t.start()

def smooth_move(target_left, target_right, steps=50):
    global left_q, right_q
    start_left = left_q[:]
    start_right = right_q[:]
    for s in range(steps):
        ratio = (s + 1) / steps
        for i in range(7):
            left_q[i] = start_left[i] + (target_left[i] - start_left[i]) * ratio
            right_q[i] = start_right[i] + (target_right[i] - start_right[i]) * ratio
        time.sleep(0.02)

def show_help():
    print("""
===== Dex3 ハンドコントローラー =====
コマンド:
  close          両手を閉じる
  open           両手を開く
  close left     左手だけ閉じる
  close right    右手だけ閉じる
  open left      左手だけ開く
  open right     右手だけ開く
  status         現在の値を表示
  help           このヘルプを表示
  quit           終了
====================================
""")

print("Dex3コントローラー起動中...")
show_help()

while True:
    try:
        cmd = input("> ").strip().lower()
    except (EOFError, KeyboardInterrupt):
        break

    if cmd in ('quit', 'q'):
        break
    elif cmd in ('help', 'h'):
        show_help()
    elif cmd == 'close':
        smooth_move(LEFT_CLOSE, RIGHT_CLOSE)
        print("両手を閉じました")
    elif cmd == 'open':
        smooth_move([0.0]*7, [0.0]*7)
        print("両手を開きました")
    elif cmd == 'close left':
        smooth_move(LEFT_CLOSE, right_q[:])
        print("左手を閉じました")
    elif cmd == 'close right':
        smooth_move(left_q[:], RIGHT_CLOSE)
        print("右手を閉じました")
    elif cmd == 'open left':
        smooth_move([0.0]*7, right_q[:])
        print("左手を開きました")
    elif cmd == 'open right':
        smooth_move(left_q[:], [0.0]*7)
        print("右手を開きました")
    elif cmd == 'status':
        print(f"左手: {[f'{q:.2f}' for q in left_q]}")
        print(f"右手: {[f'{q:.2f}' for q in right_q]}")
    elif cmd == '':
        continue
    else:
        print(f"不明なコマンド: {cmd}  (helpで一覧表示)")

running = False
print("終了します")
EOF

保存したら実行します。

conda activate unitree_sim_env
python dex_controller.py
> close        # 両手を閉じる
> open         # 両手を開く
> close left   # 左手だけ閉じる
> close right  # 右手だけ閉じる
> status       # 現在の各モーターの値を表示
> quit         # 終了

https://youtu.be/ZcZMXnvJ4zY?si=hhDn11yzXAZwRn09

まとめ

Isaac Sim上のUnitree G1に対して、DDSを用いてDex3ハンドの制御を実現しました。

今後は実機のG1への展開、Inspire製の5本指ハンドを使ったより複雑な制御の記事を出します。
G1の全身を使った実験や、Isaac Simの基本的な操作方法、強化学習や模倣学習の基礎等もバンバン記事にしていきます。

参考リンク

この記事をシェアする

関連記事