PythonログとUnity可視化の乖離。mechArm 270の動作ログが「成功」でも目視がおかしかった理由
LOG96R2R2R2026/3/15

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で解くべき問題か」を区別できるようになった。この判断基準がなければ、今日の作業は正しいのか間違いなのかわからないまま進んでいた。

ロボットを動かすとはどういうことか。

「ログが成功と言っている」と「目で見て正しく動いている」は、別の話だ。この作業記録はその区別を、数字で証明しようとしている。

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