「ゴールに引力、障害物に斥力」— これだけでロボットが迷わず進むって、本当でしょうか?
実はこのアイデア、1980年代からある古典的な手法なんです。でも今なお、工場の搬送ロボットやドローンの自動航行で現役で使われています。なぜか?
「計算が超軽い」から。
ディープラーニングみたいにGPUを必要とせず、数千円のマイコンでもリアルタイムに経路を再計算できる。これが現場で重宝される理由です。
でも、弱点もあります。それが**「局所極小」**。ゴールじゃない場所で動けなくなる、ポテンシャル場法のアキレス腱です。
今日は、このシミュレーターを実際に動かしながら、数式→実装→実験→限界まで一気に解説します。
▶ 今回のシミュレーター: ロボット経路計画 シミュレーター (ブラウザで動作・登録不要)
ざっくり本質:「磁石の坂道」を想像してください
ゴールに引力の磁石、障害物に斥力の磁石を置きます。すると空間に「でこぼこの地形」ができます。
- ゴール付近:くぼ地(低い)
- 障害物付近:山(高い)
- それ以外:なだらかな斜面
ロボットは**「高いところを避けて、低いところへ転がる玉」**のように動く。これがポテンシャル場法の本質です。
「ロボットはポテンシャルの坂を下る— ただそれだけ」
数式で理解する
ポテンシャル場法の心臓部は、たった2つの数式です。
引力ポテンシャル
U_{att}= \frac{1}{2}k_{att}\cdot d_{goal}^2
- $k_{att}$:引力の強さ(係数)
- $d_{goal}$:ロボットからゴールまでの距離
解釈:「ゴールから遠いほど、強い力で引き寄せられる」— バネの自然長からの伸びに比例する力と同じ形です。
斥力ポテンシャル
U_{rep}= \frac{1}{2}k_{rep}\left(\frac{1}{d_{obs}}- \frac{1}{d_0}\right)^2
- $k_{rep}$:斥力の強さ(係数)
- $d_{obs}$:ロボットから障害物までの距離
- $d_0$:斥力が作用する最大距離(影響半径)
解釈:「障害物に近づくほど急激に反発力が強くなる」— $d_{obs}$が0に近づくと$1/d_{obs}$が爆発的に大きくなる設計です。$d_{obs} > d_0$の範囲では斥力は0(影響なし)。
合成ポテンシャル
U_{total} = U_{att} + \sum_{i} U_{rep,i}
ロボットはこの $U_{total}$ が減少する方向(=勾配の逆方向)に進みます。
コードで実装する ★最重要★
では、実際に動くコードを見てみましょう。このシミュレーターのコア計算をJavaScriptで再現します。
// ポテンシャル場法の1ステップ計算
class PotentialFieldPlanner {
constructor(params) {
this.k_att = params.k_att || 1.0; // 引力係数
this.k_rep = params.k_rep || 0.5; // 斥力係数
this.d0 = params.d0 || 3.0; // 斥力影響半径
this.stepSize = params.stepSize || 0.1; // 1ステップの移動量
}
// 引力ポテンシャルの勾配(=引力ベクトル)
calcAttractiveGradient(pos, goal) {
const dx = goal.x - pos.x;
const dy = goal.y - pos.y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist < 0.001) return {x: 0, y: 0}; // ゴール到達
// U_att = 0.5 * k_att * dist^2 の勾配 = k_att * (pos - goal)
return {
x: -this.k_att * dx, // 勾配の逆方向(減少方向)へ進む
y: -this.k_att * dy
};
}
// 斥力ポテンシャルの勾配(=斥力ベクトル)
calcRepulsiveGradient(pos, obstacles) {
let forceX = 0, forceY = 0;
for (const obs of obstacles) {
const dx = pos.x - obs.x;
const dy = pos.y - obs.y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist > this.d0) continue; // 影響範囲外
if (dist < 0.001) {
// 障害物に激突!大きな斥力を与える
forceX += 100 * Math.sign(dx || 0.001);
forceY += 100 * Math.sign(dy || 0.001);
continue;
}
// U_rep = 0.5 * k_rep * (1/dist - 1/d0)^2 の勾配
// = k_rep * (1/dist - 1/d0) * (1/dist^2) * (pos - obs)/dist
const coeff = this.k_rep * (1/dist - 1/this.d0) / (dist*dist*dist);
forceX += coeff * dx;
forceY += coeff * dy;
}
return {x: forceX, y: forceY};
}
// 1ステップ進む
step(pos, goal, obstacles) {
const attr = this.calcAttractiveGradient(pos, goal);
const rep = this.calcRepulsiveGradient(pos, obstacles);
// 合力(勾配の総和)
const totalX = attr.x + rep.x;
const totalY = attr.y + rep.y;
const mag = Math.sqrt(totalX*totalX + totalY*totalY);
if (mag < 0.001) return null; // 局所極小またはゴール
// 正規化してstepSizeだけ移動
return {
x: pos.x + this.stepSize * (totalX / mag),
y: pos.y + this.stepSize * (totalY / mag)
};
}
}
// 使用例
const planner = new PotentialFieldPlanner({
k_att: 1.0,
k_rep: 0.5,
d0: 3.0,
stepSize: 0.1
});
let robot = {x: 0, y: 0};
const goal = {x: 10, y: 10};
const obstacles = [{x: 5, y: 5, radius: 1}];
for (let i = 0; i < 200; i++) {
const next = planner.step(robot, goal, obstacles);
if (!next) break;
robot = next;
console.log(`Step ${i}: (${robot.x.toFixed(2)}, ${robot.y.toFixed(2)})`);
}
このコードが何をしているか:
- 引力勾配を計算(ゴール方向へのベクトル)
- 各障害物からの斥力勾配を計算(障害物から遠ざかるベクトル)
- 両者を足して、その方向に微小移動
- これを繰り返す
たったこれだけで、ロボットは障害物を避けながらゴールへ向かうんです。
数値例で確かめる
実際に数値を入れて計算してみましょう。
設定:
- ロボット初期位置:$(0, 0)$
- ゴール位置:$(10, 0)$(真右)
- 障害物:$(5, 0)$に1つ(半径1)
- パラメータ:$k_{att}=1.0$, $k_{rep}=0.5$, $d_0=3.0$, $stepSize=0.1$
ステップ1の計算:
引力成分:
- $d_{goal} = \sqrt{(10-0)^2 + (0-0)^2} = 10$
- 引力勾配の大きさ = $k_{att} \cdot d_{goal} = 1.0 \times 10 = 10$
- 方向:$(10, 0)$方向 → 引力ベクトル = $(10, 0)$
斥力成分:
- $d_{obs} = \sqrt{(5-0)^2 + (0-0)^2} = 5$
- $d_{obs} > d_0(3.0)$ なので、斥力は0
合力: $(10, 0)$ → 真右に移動
- 次の位置:$(0 + 0.1 \times 1, 0) = (0.1, 0)$
ステップ50(障害物付近)の計算:
- ロボット位置:$(4.9, 0)$(障害物のすぐ左)
- $d_{obs} = 0.1 < d_0(3.0)$ → 斥力発生!
- 斥力勾配:$k_{rep} \cdot (1/0.1 - 1/3) / (0.1^3) \times 0.1 \approx 0.5 \times (10 - 0.333) / 0.001 \times 0.1 = 483.35$
- 方向:右向き(障害物から遠ざかる)→ 斥力ベクトル = $(-483.35, 0)$
- 引力ベクトル:$(5.1, 0)$方向に大きさ5.1 → $(5.1, 0)$
- 合力:$(5.1 - 483.35, 0) = (-478.25, 0)$ → 左向き!
つまり障害物に近づきすぎると、斥力が引力を上回って押し戻される。これで衝突を回避できるんです。
このまま進むと、ロボットは障害物の上側か下側に迂回します。実際のシミュレーションでは、y方向にも少しずつずれながら通過していきます。
シミュレーターで遊ぶ
実際にブラウザで動かしてみましょう。
実験1:基本動作を確認
- デフォルト設定のまま「実行」ボタンをクリック
- ロボットがゴール(右上)へ一直線に向かう
- 途中の障害物を避けて、なめらかな曲線を描く
何が起きているか: 引力と斥力のバランスが取れている状態。斥力影響半径$d_0$が適切なので、障害物に近づきすぎる前に回避行動を開始しています。
実験2:局所極小を体験
- 障害物を「U字型」に配置(画面上でドラッグ)
- ゴールをU字の中央に設定
- 実行
結果: ロボットがU字の入り口付近で停止。動かなくなる。
これが局所極小です。引力と斥力が完全に釣り合い、「坂を下り続ける」という単純な戦略では抜け出せなくなる。ポテンシャル場法最大の弱点です。
実験3:RRT比較機能を試す
- 実験2の状態で「RRT」トグルをON
- 青い経路(RRT)が生成される
違い: RRTはランダムに探索木を広げるので、U字の壁を乗り越えてゴールに到達できる。ただし経路はガタガタ(滑らかでない)。
教訓:
- 滑らかさ重視 → ポテンシャル場法
- 到達性重視 → RRT
実験4:パラメータチューニング
スライダーを動かして変化を観察:
| パラメータ | 大きくすると | 小さくすると |
|---|---|---|
| $k_{att}$ | 直線的で速いが衝突しやすい | 遠回り、低速 |
| $k_{rep}$ | 障害物から遠くを迂回 | 障害物すれすれを通る |
| $d_0$ | 遠くから反応、安全だが迂回大 | 近づいてから反応、効率的だが危険 |
現場でハマるポイント
1. 「係数を大きくすれば性能UP」は大間違い
よくある誤解:「斥力を強くすれば安全になる」
実際: $k_{rep}$を極端に大きくすると、障害物周辺の斥力の「丘」が急峻になりすぎます。すると:
- ロボットが振動(行ったり来たり)
- 細い通路を通れなくなる(両側から押されて動けない)
- 計算が不安定になる
対策: $k_{att}=1.0, k_{rep}=0.5$ を基準に、$d_0$から調整する。
2. 「計算が軽い」は罠
確かに1ステップの計算は軽い。でも局所極小にハマると無限ループ。
実装時は必ず:
- 最大ステップ数(例:1000ステップで打ち切り)
- 位置の変化が微小ならランダム擾乱を加える
- 複数回試行して結果の安定性を確認
3. 「点質量」モデルは理想化しすぎ
シミュレーターはロボットを「点」として扱います。でも実機には大きさがあります。
実務では: 障害物の半径に「ロボットの半径+安全マージン」を加えた膨張障害物を使う。例えば幅60cmの搬送ロボットなら、障害物の半径を30cm大きくする。
シミュレーターで障害物の半径を大きくして試すと、この考え方が体感できます。
まとめ
ポテンシャル場法、いかがでしたか?
3つの要点:
- 原理はシンプル:引力+斥力の合成で、ロボットはポテンシャルの坂を下る
- 実装は20行:勾配計算と微小移動を繰り返すだけ
- 限界を知る:局所極小に弱い。RRTなどと組み合わせて使うのが現実解
「計算が軽いから現場で使える」— このシンプルさが、30年以上経った今でも愛用される理由です。
▶ ロボット経路計画 シミュレーター — ブラウザで即動作、登録不要
今日の実験:
- デフォルトで実行 → 基本動作を確認
- U字障害物で局所極小を体験
- RRT比較で違いを体感
- パラメータをいじって挙動の変化を観察
NovaSolverでは1,600以上の工学シミュレーターを無料公開中 👉 一覧はこちら
「あ、これって現場のあの現象と同じだ」— そんな発見があるかもしれません。ぜひ遊んでみてください。