桃園機場捷運 3D:用 React Three Fiber 讓時刻表動起來
2026年5月1日

為什麼做這個專案
搭機場捷運時常常想:現在這條線上到底有幾班車、各自開到哪裡了?於是做了一個 3D 的機場捷運模擬:從台北車站到機場第二航廈,A1 到 A13 共 13 站,所有列車依照時刻表在 3D 路線上移動。
技術架構
| 項目 | 使用技術 |
|---|---|
| 框架 | Next.js 16 + React 19 + TypeScript |
| 3D | three.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