おことわり
本件は、私一人では到底実現できなかったことを、Claude Codeに相談しながら進めたものです。
また、本記事の大部分は、Claude Codeが作成した文章を私が加筆・修正し、さらにClaude Codeによる校正を経て仕上げています。
コードおよびコードの解説はすべてClaude code 作です。
やったこと
前提条件
- NVIDIA RTX 系 GPU(VRAM 8GB 以上推奨)+ 最新のドライバ
- ※私の環境は RTX 5070(Blackwell 世代 / RTX 50 シリーズ)です。この世代は後述のとおり CUDA 12.8 対応の PyTorch(cu128 ビルド)が必須で、そこだけ通常と手順が違います。
- ディスク空き容量 50GB 程度
- Git
- Conda(Miniconda で可)
Windows の「長いパスの有効化」
管理者 PowerShell(スタートボタンを右クリック >「ターミナル(管理者)」をクリック)で、一度だけ以下のコマンドを実行します。
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force
Miniconda のインストール
PowerShell で次を実行してください。
winget install -e --id Anaconda.Miniconda3
インストール後、そのままでは conda コマンドは PATH に入らないので、初期化を1回だけ実行します。
& "$env:USERPROFILE\miniconda3\Scripts\conda.exe" init powershell
その後、PowerShell を一度閉じて開き直してください。プロンプトの先頭に (base) と表示されれば準備完了です。
インストール
1. Python 環境を作る(Isaac Sim 5.1 / Python 3.11)
Isaac Sim 5.1 は Python 3.11 が必要です。
conda create -n env_isaac51 python=3.11 -y
conda activate env_isaac51
プロンプト先頭が (env_isaac51) になったことを確認してください。
python -m pip install --upgrade pip
2. Isaac Sim 5.1 のインストール
pip install "isaacsim[all,extscache]==5.1.0" --extra-index-url https://pypi.nvidia.com
数GBのダウンロードで数十分かかります。「Successfully installed ...」が出るまで待ってください。
3. PyTorch を GPU 版に揃える(RTX 5070 で特に重要)
今回 torch 2.7.0 が依存パッケージとして自動で入りましたが、これが GPU 対応版(CUDA 12.8)か CPU 版かで手順が変わります。
RTX 5070(Blackwell)ならではのポイント
RTX 50 シリーズ(Blackwell / GPU アーキテクチャsm_120)は、CUDA 12.8 以降に対応した PyTorch(+cu128ビルド)でないと GPU が認識されません。古い CUDA ビルドや CPU 版のままだと、torch.cuda.is_available()がFalseになったり、実行時に「no kernel image is available」系のエラーで動きません。そのため、下記の確認と(必要なら)cu128 への差し替えが RTX 5070 では必須の作業になります。
次のコマンドで確認してください。
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"
-
2.7.0+cu128 Trueのように表示 → GPU 版が入っています。次の差し替えは不要、そのまま起動確認へ進めます。 -
2.7.0+cpuやFalseと表示 → CPU 版なので差し替えが必要です。次のコマンドを実行してください。
pip install --force-reinstall torch==2.7.0 torchvision==0.22.0 torchaudio==2.7.0 --index-url https://download.pytorch.org/whl/cu128
torch の GPU 版(2.7.0+cu128)は入ったものの、差し替えで一部パッケージのバージョンがズレることがあります。以下のコマンドで、ズレたパッケージを Isaac Sim 指定のバージョンに戻してください(torch の GPU 版はそのまま残ります)。
pip install numpy==1.26.0 Pillow==11.3.0 typing_extensions==4.12.2 filelock==3.13.1 fsspec==2024.6.1 markupsafe==2.1.3 networkx==3.3 sympy==1.13.3
実行後、2つ確認します。
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"
→ 2.7.0+cu128 True ならOK(numpy を戻しても torch は GPU 版のまま動きます)。
python -c "import numpy; print(numpy.__version__)"
→ 1.26.0 ならOK。
両方確認できたら、いよいよ起動確認です。
起動
python -c "import isaacsim; isaacsim.main()"
- 初回はライセンス同意の確認が出ます(
yesで承諾)。 - その後、シェーダーコンパイルとアセット取得で起動まで10分以上かかることがあります。真っ暗なウィンドウやフリーズ風の状態も正常なので待ってください。
- GUI が開いて空のビューポートが表示されれば成功です。
Isaac Sim が立ち上がる
ダンス再生
Unitree G1 を配置
ちなみに、IssacSim はUnitreeを含めて44社のロボットのシミュレーションができます。(BostonDynamics, Boster Robotics, iRobot, Kawasaki、Yaskawaなど)
モーションは GUI では付けられません(コードで動かします)
Isaac Sim の GUI には、ロボットにダンスの動きをドラッグ&ドロップで貼り付けるような機能はありません。今回のモーションは「1フレームずつの関節角度が入った CSV」を読み込み、物理ステップごとに各関節へ角度を書き込む、というコードで再生しています。そのため、再生には必ず下記のスクリプトを実行する必要があります(メニュー操作だけでは動きません)。
モーションデータはどこから持ってきたか
使っているのは Unitree LAFAN1 Retargeting Dataset(motions/dance1_subject1.csv)です。
- 元データは Ubisoft の LAFAN1(人間のモーションキャプチャデータセット)。
- それを Unitree が G1 / H1 などの実機の関節構成に リターゲット(変換)した CSV として配布しているものです(HuggingFace:
unitreerobotics/LAFAN1_Retargeting_Dataset)。 - CSV は1行が1フレームで、列の並びは ルート位置(3) + ルート姿勢クォータニオン xyzw(4) + 関節角度(29) = 計36列。
dance1_subject1.csvは 30FPS のデータです。
コード
-
メニューの Window → Script Editor を開く
-
下のコードを全部貼り付ける
※実行前に Stage パネルで g1 をクリックして Prim Path を確認してください。
/World/g1でなければ、コード内のROBOT_PRIMをその値に書き換えます。 -
Run(Ctrl+Enter)を押す → 自動で再生が始まり、G1 が踊り出します
import asyncio
import traceback
import numpy as np
from isaacsim.core.api import World
from isaacsim.core.api.robots import Robot
CSV = r"C:\projects\Unitree-dancing\motions\dance1_subject1.csv"
ROBOT_PRIM = "/World/g1"
FPS = 30.0
JOINT_NAMES = [
"left_hip_pitch_joint", "left_hip_roll_joint", "left_hip_yaw_joint",
"left_knee_joint", "left_ankle_pitch_joint", "left_ankle_roll_joint",
"right_hip_pitch_joint", "right_hip_roll_joint", "right_hip_yaw_joint",
"right_knee_joint", "right_ankle_pitch_joint", "right_ankle_roll_joint",
"waist_yaw_joint", "waist_roll_joint", "waist_pitch_joint",
"left_shoulder_pitch_joint", "left_shoulder_roll_joint",
"left_shoulder_yaw_joint", "left_elbow_joint",
"left_wrist_roll_joint", "left_wrist_pitch_joint", "left_wrist_yaw_joint",
"right_shoulder_pitch_joint", "right_shoulder_roll_joint",
"right_shoulder_yaw_joint", "right_elbow_joint",
"right_wrist_roll_joint", "right_wrist_pitch_joint", "right_wrist_yaw_joint",
]
data = np.loadtxt(CSV, delimiter=",")
root_pos = data[:, 0:3]
quat = data[:, 3:7][:, [3, 0, 1, 2]] # CSVはxyzw → Isaacはwxyz に並べ替え
joints = data[:, 7:36] # 8列目以降の29関節
n = len(data)
duration = (n - 1) / FPS
state = {"t": 0.0, "err": False}
async def setup():
try:
World.clear_instance()
world = World(stage_units_in_meters=1.0)
robot = world.scene.add(Robot(prim_path=ROBOT_PRIM, name="dancer"))
await world.reset_async()
world.get_physics_context().set_gravity(0.0) # 倒れないよう重力オフ
print("重力オフ完了")
dof_index = {nm: i for i, nm in enumerate(robot.dof_names)}
idx = [dof_index[nm] for nm in JOINT_NAMES if nm in dof_index]
cols = [c for c, nm in enumerate(JOINT_NAMES) if nm in dof_index]
ndof = len(robot.dof_names)
print(f"マッピング済み関節: {len(idx)}/{len(JOINT_NAMES)} (全DOF: {ndof})")
def on_step(dt):
if state["err"]:
return
try:
state["t"] = (state["t"] + dt) % duration
f = state["t"] * FPS
i0 = int(f)
i1 = min(i0 + 1, n - 1)
a = f - i0
full = np.array(robot.get_joint_positions(), dtype=float)
full[idx] = (1 - a) * joints[i0, cols] + a * joints[i1, cols]
robot.set_joint_positions(full)
robot.set_joint_velocities(np.zeros(ndof))
q0, q1 = quat[i0], quat[i1]
if np.dot(q0, q1) < 0:
q1 = -q1
q = (1 - a) * q0 + a * q1
q = q / np.linalg.norm(q)
robot.set_world_pose(
position=(1 - a) * root_pos[i0] + a * root_pos[i1],
orientation=q,
)
robot.set_linear_velocity(np.zeros(3))
robot.set_angular_velocity(np.zeros(3))
except Exception:
state["err"] = True
print("=== 再生中エラー ===")
traceback.print_exc()
if world.physics_callback_exists("dance"):
world.remove_physics_callback("dance")
world.add_physics_callback("dance", on_step)
await world.play_async()
print("再生開始")
except Exception:
print("=== セットアップエラー ===")
traceback.print_exc()
asyncio.ensure_future(setup())
このコードのポイントは以下です。
-
data = np.loadtxt(CSV, ...)以降:CSV を読み込み、列を「ルート位置 / 姿勢クォータニオン / 29関節」に切り分けています。quatの[3, 0, 1, 2]は、CSV のxyzw並びを Isaac Sim のwxyz並びに直すための並べ替えです。 -
JOINT_NAMESとdof_indexの対応付け:CSV の関節の並び順を、実際のロボット(USD)の DOF 名と突き合わせて、どの列をどの関節へ入れるか(idx/cols)を作っています。 -
on_step(dt)(物理ステップごとに毎フレーム呼ばれる関数)が再生の心臓部:経過時間tから今のフレーム位置を計算し、前後フレームを線形補間(クォータニオンは正規化つき補間)して、set_joint_positionsとset_world_poseでロボットに書き込みます。これを毎ステップ繰り返すことで踊って見えます。 -
set_gravity(0.0)と速度を毎回ゼロにしているのは、物理で倒れたり暴れたりしないよう、あくまで「決めたポーズを再生する(キネマティック的な)」方針にしているためです。
他のダンス(モーション)をやりたいときも手順は同じ?
はい、同じ手順で差し替えられます。変えるのは基本 1 か所だけです。
- コード先頭の
CSV = r"...\dance1_subject1.csv"を、別のモーション CSV のパスに書き換えるだけです(例:dance2_subject3.csvなど)。あとは同じように Run するだけで、そのモーションが再生されます。
注意点:
- 差し替える CSV が 同じ Unitree LAFAN1 Retargeting Dataset の G1 用であれば、列の並び(36列・29関節)が共通なのでコード側の修正は不要です。
- 別のデータセットや別ロボット(H1 など)の CSV は、関節の数や並び順、列構成が違うため、
JOINT_NAMESや列の切り出し(data[:, 7:36]など)を、そのデータに合わせて直す必要があります。 - FPS が 30 以外のデータなら
FPS = 30.0の値も合わせてください。


