桃園機場捷運 3D:用 React Three Fiber 讓時刻表動起來

2026年5月1日

桃園機場捷運 3D

為什麼做這個專案

搭機場捷運時常常想:現在這條線上到底有幾班車、各自開到哪裡了?於是做了一個 3D 的機場捷運模擬:從台北車站到機場第二航廈,A1 到 A13 共 13 站,所有列車依照時刻表在 3D 路線上移動。


技術架構

項目使用技術
框架Next.js 16 + React 19 + TypeScript
3Dthree.js + React Three Fiber + drei
狀態管理Zustand
樣式Tailwind CSS 4
部署Vercel

主要功能

  • 列車模擬:直達車與普通車、南下與北上,依時刻表在軌道上即時移動
  • 時光機滑桿:拖曳就能切換到 06:00~24:00 的任一時間,以 60 倍速播放;按 LIVE 則回到現在的真實時間
  • 路網圖:可拖曳的半透明玻璃面板,點選車站會跳出到站看板,顯示接下來的班次(「IN n MIN」/「ARRIVING」),字體用的是七段顯示器字型
  • 日夜模式:晚上 6 點到早上 6 點自動切換成夜景,也可以手動切換
  • 四種視角:俯瞰、駕駛、跟車、車站

實作細節

1. 用時刻表算出列車現在的位置

這不是即時的列車資料,而是時刻表模擬。每班車都有每一站的到站時間,以及該站在軌道曲線上的位置 t。每一幀找出「上一站」和「下一站」,再依時間比例內插:

const currentStopIdx = schedule.stops.findIndex(s => s.time >= currentTime);
const prevStop = schedule.stops[currentStopIdx - 1];
const nextStop = schedule.stops[currentStopIdx];
const ratio = (currentTime - prevStop.time) / (nextStop.time - prevStop.time);
currentT = prevStop.t + (nextStop.t - prevStop.t) * Math.max(0, Math.min(1, ratio));

路線本身是用 13 個車站座標串成的 CatmullRom 曲線,每班車由 3 節車廂組成,沿著曲線前後錯開,並用 lookAt 對準前進方向。

2. 一條中心線,四條平行軌道

直達車與普通車、南下與北上,總共四條軌道。做法是取曲線的切線方向,和向上的向量做外積,得到水平的側向向量,再往兩側偏移:

const tangent = curve.getTangent(t).normalize();
const up = new THREE.Vector3(0, 1, 0);
const normal = new THREE.Vector3().crossVectors(tangent, up).normalize();
pt.add(normal.multiplyScalar(offset));

3. 4,000 棵樹也不卡:InstancedMesh

場景裡有 300 棟建築和 4,000 棵樹。如果每個都是獨立的網格,瀏覽器會吃不消,所以用 instancedMesh 一次畫完。分布用固定種子的亂數產生,每次載入的景色都一樣,並依區域調整密度:台北市區建築多、林口台地是森林、大園一帶是農田。

let seed = 555;
const rnd = () => { const x = Math.sin(seed++) * 10000; return x - Math.floor(x); };
// ...
else if (x > -35 && x < -10) tDensity = 0.85; // 林口森林

地形也是程式產生的:用一個高斯曲面做出林口台地的高低差,再疊上一點起伏,軌道經過的地方則壓平。


最花時間的部分:對齊時刻表

畫面做出來之後,真正花時間的是讓模擬的時間對上官方時刻表。各站的時間差、南下與北上的偏移,前後調整了好幾輪,最後逐站對照官方資料校正 A1 到 A9 的時間。

3D 的部分反而相對順利:很多時候「看起來對」比「算得精準」容易,但做的是交通資訊,就得兩者都要。


小結

這個專案結合了 3D 與資訊視覺化:同樣一張時刻表,換成在立體路線上移動的列車,就從「要查的資料」變成「一眼就懂的畫面」。

🌐 線上 Demo