ブラウザで動く「山手線 3Dジオラマ」の開発において、最も苦労した点の一つがMapLibreの地図上にThree.jsの3Dモデル(電車)を正しく配置し、スムーズに動かす部分です。
今回は、その実装の肝となる座標変換と車両の向きの制御について、実際に書いたコードを交えながら振り返ってみたいと思います。
MapLibreとThree.jsの座標系の違い
MapLibre GL JSは、地球全体をメルカトル図法で投影した座標系を持っています。一方、Three.jsは一般的な3D空間(デカルト座標系)です。この2つを同期させるためには、MapLibreが提供する CustomLayerInterface を利用します。
特に重要なのが、緯度経度をMapLibre内部の「メルカトル座標(MercatorCoordinate)」に変換する処理です。これを怠ると、ズームレベルを変えた瞬間に電車の位置がズレてしまったり、大きさがおかしくなったりします。
// 緯度経度からメルカトル座標への変換
const modelOriginMercator = maplibregl.MercatorCoordinate.fromLngLat(MODEL_ORIGIN, 0);
const METER_SCALE = modelOriginMercator.meterInMercatorCoordinateUnits();
meterInMercatorCoordinateUnits() というメソッドが非常に便利で、これを使うことで「1メートルがMapLibreの座標上でどれくらいの大きさか」を取得できます。これのおかげで、電車のサイズをメートル単位で指定しても、地図上で正しい縮尺で表示されるようになります。
電車の向きをリアルに計算する
開発初期は、単に「現在地」と「次の目的地」の2点を結んだ角度を車両の向きとしていました。しかし、これだとカーブに差し掛かったときに車両がカクカクと動いてしまい、まるで板が回っているような不自然な動きになってしまいました。
そこで導入したのが、**「台車間距離(ボギー角)」を考慮した計算ロジック**です。
2点支持による回転制御
実際の電車は、前後の台車(車輪がついている部分)がレールに乗っています。そこで、シミュレーション上でも車両の中心点1つで計算するのではなく、「前方台車の位置」と「後方台車の位置」の2点を計算し、その差分から向きを決定するようにしました。
// 前後の台車位置を計算
let distFront = centerDist + (BOGIE_DISTANCE / 2);
let distRear = centerDist - (BOGIE_DISTANCE / 2);
// 座標取得
const posFront = config.lut.getPosRot(distFront);
const posRear = config.lut.getPosRot(distRear);
// 向きの計算 (atan2を使用)
const dx = posFront.x - posRear.x;
const dy = posFront.y - posRear.y;
const angle = Math.atan2(dy, dx);
// モデルの向きに合わせて補正 (+90度など)
mesh.rotation.y = angle + (Math.PI / 2);
このロジックに変更したことで、カーブ走行時に車体が内側に切り込むような、鉄道模型特有のリアリティが出せるようになりました。特にS字カーブを抜ける時のヌルッとした動きは、見ていて気持ちが良いものです。
まとめ
MapLibreとThree.jsの連携は、最初は座標系の理解に戸惑いますが、一度変換ロジックを作ってしまえば非常に強力な表現力を手に入れられます。
今後は、この仕組みを応用して、ポイント切り替えや複線ドリフト(?)のような複雑な挙動にも挑戦してみたいと思っています。