UnityシミュレーションでmechArm 270がブロックを掴んで箱に入れた
Unityの中でロボットアームが赤いブロックを掴み、白い箱に入れた。その瞬間、デジタルツインが「動くおもちゃ」から「検証環境」に変わった気がした。
💡 Key Takeaway
実機で動作確認済みのジョイント角度データをそのままUnityシミュレーションに移植したら、ピック&プレースが成立した。現実→シミュレーションの逆引きが、今日初めて機能した。
やったこと | 起きたこと | 判断 |
|---|---|---|
赤ブロック・白箱をUnityシーンに追加 | Rigidbodyで床に落下 → 原因特定・修正 | Collider設計の基礎を再確認 |
実機ポーズCSVをUnity用に変換 | CSV読み込みで即座に動作 | 実機データの移植性が確認できた |
MotionLogPlayerにアタッチ/デタッチ実装 | グリッパーがブロックを運べるようになった | 物理的な「掴む」を仮想的に表現 |
BoxCollider追加・重力落下 | ブロックが箱の中に落ちた | シミュレーション成立 |
オブジェクトを作ることから始めた
— 掴むものと、置く場所を、まずシーンに用意する。
今まで coffeecup_3_arm1 という既存の3Dスキャンモデルで動作確認してきた。でも実機で今使っているのは、24mm角の赤いプラスチックブロックと、80mm角の白い箱だ。
シミュレーションと実機を揃えるためには、Unityの中にも同じオブジェクトが必要になる。
RedBlock_arm1 と WhiteBox_arm1 をCubeの組み合わせで作った。
RedBlock_arm1:
Scale: (0.024, 0.024, 0.024) ← 24mm角
Component: BoxCollider(衝突判定)
Component: Rigidbody
Mass: 0.05
Use Gravity: true
Is Kinematic: true(初期状態)
Collision Detection: ContinuousWhiteBox_arm1は空のGameObjectを親にして、5つのCubeを子として構成した。
WhiteBox_arm1(親・Position基準)
└ Bottom Scale: (0.080, 0.002, 0.080) LocalPosition: (0, 0.001, 0)
└ Front Scale: (0.080, 0.043, 0.002) LocalPosition: (0, 0.0215, 0.039)
└ Back Scale: (0.080, 0.043, 0.002) LocalPosition: (0, 0.0215, -0.039)
└ Left Scale: (0.002, 0.043, 0.076) LocalPosition: (-0.039, 0.0215, 0)
└ Right Scale: (0.002, 0.043, 0.076) LocalPosition: (0.039, 0.0215, 0)5つの子オブジェクトそれぞれにBoxColliderを追加することで、箱の内側でブロックが止まる衝突判定が成立する。
ここで最初の問題が出た。Rigidbodyをつけたブロックが、再生した瞬間にY=-556まで落下した。重力で床を突き抜けた。BoxColliderを追加して床との衝突判定を持たせることで解決した。
実機のデータをそのまま使った
— 「動作確認済みのポーズ」が、シミュレーション用CSVになった。
実機mechArm 270-Piで確定済みのジョイント角度がある。
HOME: [0, 0, 0, 0, 0, -30]
APPROACH_PICK: [-54.22, 81.91, -30.14, -8.08, -50.27, -29.44]
PICK: [-46.93, 102.74, -43.33, 3.6, -60.11, -31.9]
APPROACH_PLACE: [59.85, 30.0, 27.94, 10.01, -45.17, -35.33]
PLACE: [54.49, 45.17, 26.63, 2.9, -70.57, -29.26]これをUnityのCSVフォーマットに変換した。
timestamp,label,J1,J2,J3,J4,J5,J6
2026-01-01 00:00:00.000000,HOME,0,0,0,0,0,-30
2026-01-01 00:00:04.000000,GRIPPER_OPEN,0,0,0,0,0,-30
2026-01-01 00:00:08.000000,APPROACH_PICK,-54.22,81.91,-30.14,-8.08,-50.27,-29.44
2026-01-01 00:00:12.000000,PICK,-46.93,102.74,-43.33,3.6,-60.11,-31.9
2026-01-01 00:00:16.000000,GRIPPER_CLOSE,-46.93,102.74,-43.33,3.6,-60.11,-31.9
2026-01-01 00:00:20.000000,APPROACH_PLACE,59.85,30.0,27.94,10.01,-45.17,-35.33
2026-01-01 00:00:24.000000,PLACE,54.49,45.17,26.63,2.9,-70.57,-29.26
2026-01-01 00:00:28.000000,GRIPPER_OPEN_PLACE,59.85,30.0,27.94,10.01,-45.17,-35.33
2026-01-01 00:00:32.000000,HOME_RETURN,0,0,0,0,0,-30motion_log_block.csv という名前でStreamingAssetsに配置し、MotionLogPlayerに読み込ませた。再生した瞬間にアームが動いた。実機のデータがそのまま使えた。
「掴む」を仮想的に表現した
— 物理的なグリッパーの力は、コードで代替する。
Unityのシミュレーションでグリッパーがブロックを「掴む」ためには、物理的な摩擦力ではなくコードで解決する必要がある。
MotionLogPlayer.cs に2つの処理を追加した。
// GRIPPER_CLOSE: ブロックをEndEffectorの子にアタッチ
if (_labels[i] == "GRIPPER_CLOSE")
{
Rigidbody rb = cupObject.GetComponent<Rigidbody>();
if (rb != null) rb.isKinematic = true; // 重力無効
cupObject.transform.SetParent(endEffector, worldPositionStays: true);
}
// GRIPPER_OPEN_PLACE: ブロックをデタッチ、重力で落下
if (_labels[i] == "GRIPPER_OPEN_PLACE")
{
cupObject.transform.SetParent(null, worldPositionStays: true);
Rigidbody rb = cupObject.GetComponent<Rigidbody>();
if (rb != null)
{
rb.isKinematic = false;
rb.useGravity = true; // 重力有効→自然落下
}
}GRIPPER_CLOSEでブロックをEndEffectorの子にする。アームが動けばブロックも追従する。GRIPPER_OPEN_PLACEでデタッチして重力を有効にする。ブロックは自然に落ちる。
ブロックが箱に入った
— 何度も詰まって、何度も直して、最後に成立した。
途中でいくつかの問題が出た。CSVのパスが間違っていてファイルが読めなかった。ブロックにBoxColliderがなくて床を突き抜けた。WhiteBox_arm1の壁にBoxColliderがなくてブロックが箱を素通りした。
一つ一つ直した。
最終的に、再生を押したら。
グリッパーが開いてブロックに近づく。グリッパーが閉じてブロックをアタッチする。アームが箱の上まで運ぶ。グリッパーが開いてブロックをデタッチする。ブロックが重力で箱の中に落ちる。
シミュレーションが成立した。
いった!
この成立が意味すること
実機で確定したジョイント角度データがある。そのデータをCSVに変換したら、Unityでも同じ動作が再現できた。
これが本質だ。
実機→シミュレーションの移植が機能した。逆方向(シミュレーション→実機)は確認済みだった。今両方向が繋がった。
R2R2R(Real2Render2Real)のループ構造が、ブロックと箱という小さなスケールで、初めて完全に動いた。
