mechArm 270のアダプティブグリッパーをUnityデジタルツインに組み込んだ
LOG134R2R2R2026/4/9

mechArm 270のアダプティブグリッパーをUnityデジタルツインに組み込んだ

手動仮組みをやめて、URDFから自動インポートした。

💡 Key Takeaway

グリッパーの組み立ては「手作業でやるもの」だと思い込んでいた。ロボットアームと同じURDFが存在した。同じ手順で自動インポートすればよかった。

やったこと

起きたこと

判断

URDFを探した

mycobot_adaptive_gripper.urdf があった

手動仮組みをやめた

package://パスをファイル名に書き換えた

URDF Importerで読み込めた

そのまま進んだ

ArticulationBodyが残っていた

グリッパーが暴走・link6も壊れた

スクリプトで全削除した

J6のRotationが再生中にズレた

+30°のオフセットが必要だった

MotionLogPlayerで補正した

GripperControllerを実装した

全6パーツが連動して開閉した

Open Angle=35で確定した

手動仮組みをやめた

— URDFがあるなら、使えばいい。

LOG133でグリッパーを手動で仮組みしていた。gripper_base をlink6の子にドラッグして、座標を目視で合わせて、回転をInspectorで調整する。GripperBuilder.cs を書いて、座標値を何度も書き直して。

「もっと簡単に組めないの?ロボットアームのときはお任せしたよ?」

そうだ。ロボットアームは mycobot_description のURDFから自動インポートした。グリッパーも同じパッケージにある。

find ~/mycobot_ros -name "*.urdf" | grep -i gripper

あった。mycobot_ros/mycobot_description/urdf/adaptive_gripper/mycobot_adaptive_gripper.urdf

URDFを変換してUnityに渡す

— package:// パスをそのままでは読めない。ファイル名だけにした。

URDFの中身を確認すると、メッシュのパスがこうなっていた。

<mesh filename="package://mycobot_description/urdf/adaptive_gripper/gripper_base.dae"/>

Unity URDF ImporterはROSのパッケージ解決機能を持たない。DAEファイルはすでに Assets/Models/gripper/ に置いてあるので、パスをファイル名だけにすればいい。

sed 's|package://mycobot_description/urdf/adaptive_gripper/||g' \
  .../mycobot_adaptive_gripper.urdf \
  > .../Assets/Models/gripper/adaptive_gripper.urdf

確認:

<mesh filename="gripper_base.dae"/>
<mesh filename="gripper_left1.dae"/>
...

Projectウィンドウで右クリック → Import Robot from Selected URDF file → Axis Type: Y Axis → Import URDF。

Hierarchyに firefighter(URDFのrobot名)が生成された。gripper にリネームして、link6の子にドラッグした。

ArticulationBodyが暴走した

— インポートしたままでは動けない。

再生すると、グリッパーが全開して実機なら壊れる角度まで動いた。URDFインポート時に付いた UrdfJointRevolute と ArticulationBody が、独自のControllerスクリプトで動いていた。

link6も再生中にRotationが変わり、グリッパーが斜めになった。

2つを同時に解決するために、Editorスクリプトを書いた。

[MenuItem("R2R2R/Remove Gripper ArticulationBodies")]
static void RemoveAll()
{
    var gripper = GameObject.Find("gripper");
    foreach (var c in gripper.GetComponentsInChildren<UrdfJointRevolute>())
        Object.DestroyImmediate(c);
    foreach (var c in gripper.GetComponentsInChildren<UrdfJoint>())
        Object.DestroyImmediate(c);
    foreach (var c in gripper.GetComponentsInChildren<ArticulationBody>())
        Object.DestroyImmediate(c);
    Debug.Log("Done");
}

R2R2R → Remove Gripper ArticulationBodies を実行。エラーなし。

J6に+30°のオフセットが必要だった

— グリッパーを付けて初めてわかったズレがある。

再生中、グリッパーが斜めになる。HOME姿勢では正しく見えるが、link6が動くと破綻する。

再生前のlink6: Rotation X:90 → 再生後: X:59。約30度ズレている。

MotionLogPlayer.cs のCSV読み込み処理でJ6(インデックス5)にオフセットを加えた。

float angle = 0f;
float.TryParse(cols[j + 2], out angle);
if (j == 5) angle += 30f;  // グリッパー取り付け座標系の補正
angles[j] = angle;

再生した。グリッパーの傾きが直った。

GripperControllerを実装した

— ArticulationBodyを削除したので、開閉は自前で動かす。

URDFのjoint定義を確認した。

<joint name="gripper_controller" type="revolute">
    <axis xyz="0 0 1"/>
    <limit effort="1000.0" lower="-0.7" upper="0.15"/>
    <parent link="gripper_base"/>
    <child link="gripper_left3"/>
</joint>

mimic jointで他のパーツが連動している。6パーツすべてを GripperController.cs で制御する。

public class GripperController : MonoBehaviour
{
    public Transform gripperLeft3, gripperLeft2, gripperLeft1;
    public Transform gripperRight3, gripperRight2, gripperRight1;

    public float openAngle = 35f;
    public float speed = 90f;

    private float _currentAngle = 0f;
    private float _targetAngle = 0f;
    private Quaternion _left3Init, _left2Init, _left1Init;
    private Quaternion _right3Init, _right2Init, _right1Init;

    void Start()
    {
        _left3Init  = gripperLeft3.localRotation;
        _left2Init  = gripperLeft2.localRotation;
        _left1Init  = gripperLeft1.localRotation;
        _right3Init = gripperRight3.localRotation;
        _right2Init = gripperRight2.localRotation;
        _right1Init = gripperRight1.localRotation;
        _currentAngle = 0f;
        _targetAngle  = 0f;
        ApplyAngle(0f);
    }

    public void Open()  => _targetAngle = 0f;
    public void Close() => _targetAngle = openAngle;

    void Update()
    {
        _currentAngle = Mathf.MoveTowards(_currentAngle, _targetAngle, speed * Time.deltaTime);
        ApplyAngle(_currentAngle);
    }

    void ApplyAngle(float a)
    {
        gripperLeft3.localRotation  = _left3Init  * Quaternion.Euler(0,  a, 0);
        gripperLeft2.localRotation  = _left2Init  * Quaternion.Euler(0,  a, 0);
        gripperLeft1.localRotation  = _left1Init  * Quaternion.Euler(0, -a, 0);
        gripperRight3.localRotation = _right3Init * Quaternion.Euler(0, -a, 0);
        gripperRight2.localRotation = _right2Init * Quaternion.Euler(0, -a, 0);
        gripperRight1.localRotation = _right1Init * Quaternion.Euler(0,  a, 0);
    }
}

MotionLogPlayer.cs からラベルで呼び出す。

if (gripperController != null)
{
    if (_labels[i].Contains("GRIPPER_OPEN"))  gripperController.Open();
    else if (_labels[i].Contains("GRIPPER_CLOSE")) gripperController.Close();
}

GRIPPER_OPENで開き、GRIPPER_CLOSEで閉じる。Open Angle=35でほぼ実機通りの開閉量になった。

デジタルツインにグリッパーが生えた

今日まで、UnityのmechArmはアームだけだった。link6の先に何もない状態で、掴む動作をシミュレーションしていた。

今日からグリッパーがある。開いて、閉じる。実機のmechArmと同じ構造が、デジタル空間に揃った。

精度の話をすると——J6のオフセット+30°はキャリブレーションの一例に過ぎない。グリッパーの取り付け座標系は手動調整だし、開閉の動きは実機のAdaptive Gripperの複雑な機構を6パーツの単純な回転で近似している。完全な再現ではない。

でも「精度より体験の成立を優先する」がR2R2Rの設計哲学だ。

デジタルツインが掴む動作をする。それが成立した。

※ プロジェクト名「R2R2R」は、UC Berkeley・Toyota Research Instituteの研究「Real2Render2Real」(Yu, Fu et al., 2025)の概念に触発されています。現実空間をデジタルツイン化し、仮想空間での試行から得た知見を実世界のモーター駆動へ還元する——その設計と実装過程を毎日記録しています。技術アプローチおよび目的は独自のものです。