PythonログとUnity可視化の乖離。mechArm 270の動作ログが「成功」でも目視がおかしかった理由
このプロジェクトは、iPhoneだけを使ってロボットアームに「物を掴んで運ぶ」動作を学習させる試みだ。Unityというゲームエンジンをシミュレーター代わりに使い、6軸のロボットアームがコーヒーカップを掴んで指定場所に置く動作を作っている。
前回からの引き継ぎ
ロボットアームの関節には「どのくらい硬く動くか(Stiffness)」「どのくらい揺れを抑えるか(Damping)」というパラメータがある。このプロジェクトでは Stiffness 20,000 / Damping 2,000 を「物理の憲法」として絶対に変えないルールで運用している。実機のモーターが想定する制御範囲を守るためだ。
ところが前回、このパラメータがシミュレーション内で一切機能していなかったことが判明した。UnityはデフォルトのTarget modeでは、InspectorにどんなStiffness値を書いても内部で 1e+25(実質無限大)を使う。憲法値は存在していたが、物理には届いていなかった。
解決策はシンプルだった。Force modeに切り替えれば、設定したStiffness/Dampingが実際に物理演算に反映される。切り替えてみた。
アームが倒れた。
「壊れた」を計測する
感覚で判断せず、データで原因を追うのがこのプロジェクトのやり方だ。
まず疑ったのは forceLimit(関節が出せる最大トルクの上限)だ。Force modeに切り替えた状態で、この値が 1,000,000(N·m)という異常に大きな値のままになっていた。
段階的に下げながら挙動を確認した。
forceLimit = 5,000 → アームが自重で崩れる。トルクが重力に負ける。
forceLimit = 50,000 → ピックアンドプレースが3サイクル完走した。50,000で動いた。しかし、アームが動きながら横に傾く問題が残っていた。ログでは「カップの把持・移動・設置」が完走しているのに、見た目がおかしい。
この「ログ正常・見た目異常」の状態を解明するために、診断ツールを追加した。
全6関節の「ズレ」をCSVで記録した
ロボットアームには6つの関節がある。それぞれの関節に対して、毎フレームこの値を記録するコードを追加した。
divergence(乖離)= drive.target(コードが指示する角度)
- jointPosition(関節が実際にいる角度)これは「コードが意図していること」と「物理が実際にやっていること」の差だ。
1サイクル分のデータを解析した結果:
関節 最大乖離
j0 -72°
j1 +59°
j2 +47°
j3 ±117° ← 最も深刻
j4 +96°
j5 0° ← 末端関節のみ正常末端を除く全関節で、指示角度と実際の角度が最大117°もズレていた。
なぜズレるのか
Force modeのトルク計算式はこうなっている。
トルク = Stiffness × (drive.target - jointPosition)つまり 「ズレが大きいほどトルクが強くなる」 という式だ。
このプロジェクトのCCD IK(ロボットアームの手先を目標に近づけるアルゴリズム)は、毎フレームこう書いていた。
drive.target += angle * speed; // 毎フレーム加算し続けるTarget modeでは、drive.target は「到達したい角度」を意味する。少しずつ加算して目標に近づける、この書き方は正しく機能する。
Force modeでは、drive.target の意味が変わる。「トルクの基準点」になる。加算し続けると基準点が実際の関節位置からどんどん離れ、その差(乖離)に比例してトルクが増大し続ける。
j3の乖離が117°になったとき、実際に発生していたトルクは:
20,000 × 117° = 2,340,000 N·mこれが「アームが横に弾ける」原因だった。
修正を試みた。止まった。
乖離を防ぐために、毎フレームリセットする書き方に変えた。
// 累積をやめて、毎フレーム現在位置からの相対値にする
drive.target = GetDOF(targetJoints[i].jointPosition) + angle * 1.0f;乖離は解消された。しかしアームが全く動かなくなった。
dist = 0.141(目標まで14cm)で完全に停止したまま動かない。
理由はこうだ。毎フレームリセットすると、蓄積がゼロになる。蓄積がなければ、CCD IKは関節を「少しずつ積み上げて目標に近づける」ことができない。目標に向かう推進力そのものを消してしまった。
構造的に合わない、という結論
ここで問題の本質が見えた。
CCD IKというアルゴリズムは「少しずつ drive.target を積み上げながら目標に近づく」設計になっている。Target modeではこれが「目標角度への段階的な接近」として機能する。
Force modeでは drive.target が「トルクの基準点」なので、積み上げることがトルクの爆発につながる。かといって積み上げをやめると推進力がなくなる。
Target mode + CCD IK → ✅ 動く(設計通り)
Force mode + CCD IK → ❌ 構造的に合わないForce modeでCCD IKを動かしたいなら、IKのアルゴリズムごと変える必要がある。「少しずつ積み上げる」ではなく「目標角度を一度に計算してセットする」方式、つまり解析的IKまたはJacobianベースのIKが必要だ。実装には5〜10日かかる。
ではなぜ、今は進めることができたのか
答えはシンプルだ。Force modeは「実機に展開するとき」に必要になる。今は必要ない。
このプロジェクトは3つのフェーズで動いている。
フェーズ1(現在): Unityシミュレーションでデモを動かす
フェーズ2 : 同じワークフローを他の人が使えるか検証する
フェーズ3 : 実機(物理的なロボットアーム)に展開する実機展開時、UnityのArticulationBodyでモーターを直接動かすことはない。ROS(ロボット制御の業界標準OS)のMoveItというライブラリを経由する。MoveItには解析的IKが内蔵されており、Force modeとの互換性問題はそこで解決される。
Stiffness 20,000 / Damping 2,000 が物理に正確に届いているかどうかは、フェーズ3で初めて本質的な意味を持つ。フェーズ1のデモには、Target modeで3サイクル完走できれば十分だ。
「精度より体験の成立を優先する。精度はあとからお金で解決できる」——これがR2R2Rの設計哲学だ。
この作業で残ったもの
Force modeへの移行は持ち越しになった。しかし代わりに2つ得た。
診断基盤の整備: 全6関節の乖離をフレームごとにCSVで記録するツールが完成した。今後どんな物理問題が起きても、原因特定の時間が大幅に短縮される。作業ログとして記録されたデータは、今日だけで52万行を超えた。
判断軸の明確化: 「Unityシミュレーションで解くべき問題か、実機展開時にROS+MoveItで解くべき問題か」を区別できるようになった。この判断基準がなければ、今日の作業は正しいのか間違いなのかわからないまま進んでいた。
ロボットを動かすとはどういうことか。
「ログが成功と言っている」と「目で見て正しく動いている」は、別の話だ。この作業記録はその区別を、数字で証明しようとしている。
