本文へ移動
BenriWorks Lab

開発記事公開日

物理エンジンでクレーンの荷振れとワイヤー破断を表現する

スタンディングデスクで、物理シミュレーションを映したワイドモニタを見る男性エンジニア

クレーン安全シミュレーターで、振り子近似からRapier3Dの剛体とバネダンパーケーブルへ移行し、荷振れと破断を創発させた実装を紹介します

荷振れを描くだけなら、振り子の式で足りていました。「Three.jsで移動式クレーンの荷振れを表現する方法」に記録した初期実装は、ブーム先端を支点とする振り子に減衰項を加えた解析近似で、操作すると揺れて待つと収まる因果を軽量に再現できていました。ところが、教育シミュレーターとしてワイヤー破断や転倒まで体験させようとすると、現象ごとに近似を手書きする作りは急に苦しくなります。行き着いた答えは、Rapier3Dの剛体シミュレーションへの載せ替えと、ワイヤーを引張のみのバネダンパーとして自前で計算する構成でした。この記事では、荷振れも破断も「張力」という一つの量から立ち上がるようになるまでの設計判断を記録します。

破断と転倒を足したときの振り子近似

振り子近似は、荷振れ単体では欠点らしい欠点がありませんでした。壊れ始めたのは、体験させたい現象が増えたときです。ワイヤー破断を描くには張力の値が要りますが、振り子モデルはワイヤーを長さ固定の拘束として扱うため、張力という量がそもそも登場しません。巻き下げてたるませれば実物のワイヤーは垂れて力を伝えなくなりますが、支点からの距離が一定という前提はたるみを表せません。転倒判定には吊り荷がクレーンを引く反力が、破断後には荷の自由落下と地面への衝突が要ります。

それぞれに近似を足すことは、たぶんできます。ただ、破断の瞬間に振り子モデルから自由落下モデルへ荷を引き渡し、着地で衝突応答につなぎ、その間も転倒判定へ反力を渡し続ける接続部は、全部自前で書くことになります。現象の数だけ近似を足せても、受け渡しはその組み合わせだけ増えます。ここで個別近似をやめました。土台に採用したのはRapier(Rust製物理エンジンのWASMビルド)で、@dimforge/rapier3d-compatはWASMバイナリを内蔵しているため、バンドラー設定なしの動的importで動きます。物理は1/120秒の固定タイムステップで進め、1フレームに最大8サブステップ回します。

ケーブルを距離拘束にしなかった理由

剛体エンジンに載せると決めた時点では、ワイヤーはジョイント(距離拘束)で表せば済むと考えていました。剛体二つの距離を保つ拘束は、どのエンジンにも標準で備わっているからです。この見立ては二つの性質に破られました。一つめはたるみです。距離拘束は縮む方向にも働くため、巻き下げて荷を地面に置いてもフックが押し戻され、ワイヤーが突っ張り棒になります。二つめは剛性です。ワイヤーロープ相当の高い剛性を、剛体ジョイントと明示的なバネの組み合わせで表す構成は、ソルバーと共振して発散します。

そこでワイヤーは、エンジンの拘束機能を使わず、ブーム先端のアンカーとフックの間に働く単一の力として毎ステップ自前で計算することにしました。距離が自然長(繰り出したロープ長を掛け数で割った値)以下なら力はゼロ。伸びていれば、フック則の復元力と相対速度に比例する減衰を、アンカーへ向かう方向にだけ加えます。

const stretch = distance - params.restLengthM;

// ロープ軸方向の相対速度(正 = さらに伸びる方向)
const relVelAlong =
  (hookVel.x - anchorVel.x) * -nx +
  (hookVel.y - anchorVel.y) * -ny +
  (hookVel.z - anchorVel.z) * -nz;

let tension = params.stiffnessNpM * stretch + params.dampingNspM * relVelAlong;
// ロープは押せない(圧縮力なし)、上限クランプで数値安定化
tension = Math.min(Math.max(tension, 0), params.maxForceN);

張力をゼロと上限でクランプする最後の一行が、この設計の要です。ゼロ側のクランプが片側性(ロープは引けるが押せない性質)で、たるみは力ゼロの状態として自然に表れます。上限側(静的荷重の8倍+5000 N)は数値事故への保険です。たるんだワイヤーの垂れ下がりは、剛体系から切り離したverlet積分の粒子列が見た目だけを描いており、剛体側には影響しません。

剛性と減衰は固定値ではなく、吊り質量とロープ長から毎ステップ決め直します。

export function computeCableStiffness(
  suspendedMassKg: number,
  ropeLengthM: number,
  gravity = 9.81
): number {
  const mass = Math.max(suspendedMassKg, 10);
  const length = Math.max(ropeLengthM, 0.5);
  return (mass * gravity) / (0.01 * length);
}

/** 減衰係数(0.5×臨界 — 地切り時のスパイクを抑えつつ揺れは残す) */
export function computeCableDamping(stiffnessNpM: number, suspendedMassKg: number): number {
  const mass = Math.max(suspendedMassKg, 10);
  return 0.5 * 2 * Math.sqrt(stiffnessNpM * mass);
}

剛性は「静的荷重で約1%伸びる」値で、実ワイヤーロープ相当の見かけ剛性としてこの比率を採用しました。減衰は臨界減衰の0.5倍です。臨界まで強くすれば地切り(たるみから張りへ切り替わる瞬間)の張力スパイクは消えますが、荷振れの揺れまで消えます。弱くすれば揺れは残るものの、地切りのたびに張力が跳ねます。0.5はその間で体験の自然さから決めた折衷です。

ワイヤー表現の比較図。距離拘束は縮む方向にも働くためたるみを表せず高剛性ではソルバーと共振するのに対し、自前のバネダンパーは自然長以下では力がゼロ、伸びているときだけ復元力と減衰をアンカー方向に加える
ワイヤーは拘束ではなく、片側にだけ働く力として毎ステップ計算する

ブーム先端を与えるキネマティックなアンカー

ケーブルのもう一端、ブーム先端は物理で解く対象ではありません。旋回や起伏の操作で決まる位置です。そこでアンカーをキネマティック剛体(位置を外から与えると、速度はエンジンが位置の履歴から推定する剛体)にし、クレーンの姿勢から計算したブーム先端位置を、サブステップごとに補間しながら毎回与えます。

この設計の効果は荷暴れに表れます。急旋回や急停止でアンカーの速度が変わると、その加速度がケーブルの張力を通じて荷へ伝わり、荷が大きく振れ回ります。旧実装では「支点の加速度を振れの入力にする」という因果を式に手で書いていましたが、今回は何も書いていません。アンカーを動かすだけで、同じ因果が構成から出てきます。

ただし、減衰項に使うアンカー速度は前ステップ位置との差分からも計算しており、ここに罠が一つありました。シナリオ切替でブーム先端がテレポートすると差分速度が巨大になり、減衰力のスパイクでワイヤーが誤って切れます。テレポート時には位置履歴と平滑化張力を明示的に捨てる再同期処理を通しています。

玉掛け中は、ケーブル力を積み荷の重心へ直接作用させ、フックはロープ上の位置へキネマティックに追従させます。フックと荷を剛体ジョイントで結んでバネを重ねる構成は、先に述べた共振で発散するため使っていません。

張力を根拠にするワイヤー破断判定

破断判定は、ばねが毎ステップ返す張力をそのまま使います。総張力を掛け数で割った1本あたり張力が、破断荷重に状態係数を掛けた閾値を超えたかを見るだけです。

export function checkWireBreak(
  tensionN: number,
  partsOfLine: number,
  ropeBreakingN: number,
  wireConditionFactor = 1
): WireBreakCheck {
  const parts = Math.max(1, partsOfLine);
  const perLineTensionN = tensionN / parts;
  const thresholdN = ropeBreakingN * Math.min(Math.max(wireConditionFactor, 0.05), 1);
  return { perLineTensionN, broken: perLineTensionN > thresholdN, thresholdN };
}

破断荷重は直径10mmの6×37ワイヤーロープ相当として47000 Nを設定し、状態係数は健全なら1、老朽ワイヤーの体験シナリオでは0.35に下げています。総張力を掛け数で割る一行があるおかげで、「2本掛けにすれば1本あたりの張力は半分になる」という玉掛けの基本が、そのまま判定に効きます。

ただ、瞬間値で切るわけにはいきません。ばねが高剛性なため、地切りや荷振れの一瞬でも閾値超過が起こり得るからです。数値的な単発スパイクを拾わないよう、判定には時定数約0.08秒で平滑化した張力を使います(HUDの張力計は別系統で約0.15秒)。そのうえで、超過が始まっても即座には切らず、超過が1.5秒継続したときに破断させるラッチを挟みました。

この1.5秒が予兆の時間です。超過開始で警告イベントを発火して画面に予兆を見せ、その間にレバーを戻して張力が閾値を下回れば、タイマーはゼロに戻って断線は起きません。「警告が出たら操作を止めれば間に合う」という、安全教育で伝えたい行動がそのまま物理側の仕様になっています。

破断後のコードは、ほとんど何もしていません。ケーブル力を恒久的にゼロにして玉掛けを解除するだけで、荷とフックはただの剛体として自由落下し、地面と衝突します。落下にも着地にも専用の式はありません。ワイヤーの見た目は、verlet粒子列の下端のピン留めを外して鞭のように舞わせます。

ワイヤー破断判定の図。1本あたり張力は総張力を掛け数で割った値、しきい値は破断荷重に状態係数を掛けた値で、張力が跳ねる場面として急な巻き上げ、荷振れの折り返し、地面への引っかかりを挙げている
破断は演出ではなく、毎ステップ計算している張力から決める

Rapierに依存しないテスト

この構成の検証のしやすさは、ケーブル計算と破断判定をRapier非依存の純粋関数と純粋クラスに切り出したことから来ています。WASMの初期化なしにJestで回せるため、次の性質を数値で直接検証できます。

  • たるみでは力がゼロになり、縮む方向の相対速度がいくら大きくても張力が負にならない(ロープは押せない)
  • 剛性が静的荷重で1%伸びる値になっており、減衰が臨界減衰の3割から10割に収まる
  • 2.8 tの荷とフック40 kgの静的張力は、老朽ワイヤー(係数0.35)の1本掛けでは閾値を超え、健全な2本掛けでは超えない
  • ラッチは1/120秒刻みで、ちょうど保持時間の1.5秒(1ステップの誤差内)で1回だけ破断し、途中で超過が解消すればタイマーはゼロからやり直しになる

三つめは、体験シナリオそのものの妥当性テストです。「老朽ワイヤーで2.8 tを1本掛けすると切れる」という教材の筋書きが、演出ではなく物理パラメータの帰結として成立していることを、シナリオを動かさずに確かめられます。

作業ジャケット姿の男性エンジニアがワイヤーロープを手にしているイラスト。左に「ワイヤーは拘束ではなく力 物理エンジンで荷振れと破断を出す」というキャッチコピーが入っている
距離拘束ではたるみを表せない

剛体エンジンへの移行で変わったこと

振り返ると、移行で得たものは、現象の数に対して受け渡しを書かなくてよくなったことに尽きます。荷振れはキネマティックなアンカーの加速度から、破断は同じばねが返す張力から、落下は力を止めた後の重力から出てきます。張力のクレーン側反力は転倒判定への入力にもなっており、冒頭で数え上げた接続部は、剛体と力という共通の土台に吸収されました。

一方で、このワイヤーが教育用の簡略化であることは変わりません。1%伸びの剛性も0.5×臨界の減衰も体験の自然さを基準に決めた値です。スリングは常に荷の重心を吊る近似のため吊り荷の傾きは表現されず、実機のワイヤーロープの破断挙動を再現するものでもありません。このシミュレーターが伝えたいのは実機の精密な再現ではなく、張力と警告と操作の因果です。

この実装は「移動式クレーン安全シミュレーター」で動いています。ブラウザ3Dに物理表現を足したくなったとき、個別の現象を描き続けるか、現象が出てくる土台へ載せ替えるか。ワイヤー1本分の設計判断が、その分かれ目の目安になれば十分です。

関連アプリ

こんな記事も読まれています