◆ ✦ ◆
Creative Tools5 min read

City Map 3D — 從 OpenStreetMap 到 Three.js 的 3D 城市

May 27, 2026

◆ ✦ ◆
City Map 3D — 從 OpenStreetMap 到 Three.js 的 3D 城市

I found a repo that turns OpenStreetMap data into 3D buildings. So I put it on my site — with search, fog, and Morandi colors.

creative-toolsbehind-the-scenes

A city that grows from data

Where it started

I was browsing GitHub and found map3d by cartesiancs — an open-source tool that takes OpenStreetMap building data and renders it as a 3D city with React Three Fiber. You pick an area on a map, it fetches every building footprint from the Overpass API, extrudes them into 3D shapes based on their real heights, and lets you orbit around the result. You can even export the whole scene as a GLB file.

I stared at the demo for a while. Then I thought: this belongs on my site.

在 GitHub 看到一座城市

我在逛 GitHub 時發現了 map3d——一個把 OpenStreetMap 建築資料渲染成 3D 城市的開源工具。選一塊區域,它就去 Overpass API 抓所有建築物的輪廓,根據真實高度擠出 3D 幾何體,你可以用滑鼠自由旋轉瀏覽。

我看了很久,覺得好酷。然後想:我也想把它放到我的網頁上。


What I changed

Not a fork — a rewrite into my stack

map3d runs on Vite + React. My site is Next.js 16. I couldn't just drop the code in — Three.js explodes on server-side rendering. So every 3D component goes through next/dynamic with { ssr: false }.

The core rendering logic is similar to the original: OSM polygons become THREE.Shape, extruded with ExtrudeGeometry, rotated to lie flat. But almost everything around it is new.

不是 fork,是重寫

map3d 用 Vite + React,我的網站是 Next.js 16。Three.js 碰到 SSR 會爆炸,所以每個 3D 元件都要用 next/dynamic + { ssr: false } 包起來。

核心渲染邏輯跟原版類似:OSM 多邊形 → THREE.ShapeExtrudeGeometry。但外面包的東西幾乎全部重寫了。


The Morandi treatment

The original map3d uses a dark background with bright green roads and grey buildings. Fine for a utility, but it didn't match my site.

I rebuilt the palette:

  • Buildings: dusty lavender #9585A6 — the same mist purple from my design system
  • Hover state: slightly brighter #A186B4
  • Roads: antique gold #C9A96E — thin lines weaving between blocks
  • Ground: warm cream #E8E6DF
  • Fog: same cream, fading buildings at the edges into nothing

The fog was the key decision. Without it, the ground plane has a visible edge — a hard line where the world ends. With fog, distant buildings dissolve into mist, and the scene feels infinite.

莫蘭迪配色

原版 map3d 用深色背景加亮綠色道路。當工具用沒問題,但跟我的網站風格不搭。

我重新設計了整套配色——建築用莫蘭迪紫、道路用古金色、地面用暖灰奶油色。再加上 Three.js 的線性霧化效果,讓邊緣的建築和道路漸漸淡出成跟背景同色的奶油白,遠的看不清、近的很清晰。

霧是最關鍵的決定。沒有它,地面有一條硬邊,城市看起來像擺在桌上的模型。有了它,畫面邊緣自然消融,感覺城市一直延伸到視線以外。


The camera problem

This was the hardest part — and it's invisible when it works.

Three.js OrbitControls manages the camera. When you set a camera position manually, OrbitControls doesn't know about it. On the very next frame, it overwrites your position with its own internal state. So you set the camera to look at your buildings, and one frame later it snaps back to the origin.

I tried five different approaches before landing on one that works: use useFrame to set the camera position once on the first frame, call camera.lookAt() and controls.update() in the same callback, and track a positioned flag so it only fires once. When the user picks a new city, the flag resets and the camera re-fits.

The distance is calculated from the scene span — the geographic extent of the selected area, converted to world units. Too close and you're inside the buildings; too far and they're dots.

相機是最難的部分

Three.js 的 OrbitControls 會管理相機位置。你手動設了一個位置,下一幀它就用自己的內部狀態覆蓋掉。所以你讓相機看向建築群,一幀後它就跳回原點。

我試了五種方法,最後用 useFrame 在第一幀設定相機、呼叫 lookAt() + controls.update()、用一個 flag 確保只執行一次。換城市時重置 flag,相機重新定位。

距離根據場景跨度自動算——太近會鑽進建築裡,太遠就只看到一堆小點。


Search anywhere

The original map3d only lets you draw a rectangle on a Leaflet map. I added two things:

Preset cities — four buttons (Taipei Xinyi, Tokyo Shibuya, Paris Arc de Triomphe, NYC Manhattan) that instantly set the area and fly the map there.

Location search — type any place name, hit Enter, pick from the dropdown. Uses the Nominatim geocoding API (free, no key needed). The bounding box is auto-clamped to a reasonable size so you don't accidentally query half of Europe.

You can even type a full Taiwan address like "台中市太平區長龍路一段166巷51號." Nominatim doesn't index individual house numbers in Taiwan, so the search automatically strips lane/alley/house numbers and retries at street level. You won't land on the exact door, but you'll get the right neighborhood.

搜尋任何地點

原版 map3d 只能在地圖上框選。我加了兩個功能:

預設城市 — 台北信義、東京澀谷、巴黎凱旋門、紐約曼哈頓,一鍵切換。

地點搜尋 — 輸入地名(中英文都行),選擇結果,自動飛過去設定區域。用的是 Nominatim 地理編碼 API,免費不需要 key。

也可以直接輸入台灣地址,例如「台中市太平區長龍路一段166巷51號」。Nominatim 沒有收錄台灣的門牌號碼,所以搜尋會自動去掉巷弄號碼、退到路名重新查詢。不會精確到門口,但會到對的街區。


The bounding box bug

Here's a fun one. My preset cities had the northeast and southwest longitudes swapped. The Overpass API was still technically returning data — but instead of 300 buildings in Taipei Xinyi, it returned 36,000. The query was fetching the complementary strip of the globe.

The 3D scene still rendered. It just had buildings scattered across an impossibly wide area, with the camera looking at empty space in the middle. It took me an embarrassingly long time to notice the building count was off by two orders of magnitude.

經緯度的小 bug

有一個有趣的 bug:我的預設城市把東北角和西南角的經度寫反了。Overpass API 還是有回傳資料——但不是台北信義的 300 棟建築,而是 36,000 棟。查詢範圍變成了地球的另一半。

3D 場景還是有渲染出來。只是建築散佈在一個超大的範圍裡,相機看到的是中間的空地。我花了很久才注意到建築數量差了兩個數量級。


Building tooltips

Hover over any building and a tooltip appears — built with @react-three/drei's Html component, which places a DOM element in 3D space. It shows the building name (if OSM has one), type, height, floor count, and street address.

The tooltip follows the Morandi design system: translucent cream background with backdrop-filter: blur, gold border, serif heading, and system-ui body text. It's a small detail, but it makes the difference between "a tech demo" and "something that belongs on this site."

建築資訊提示

滑鼠移到建築上會跳出資訊卡——用 @react-three/dreiHtml 元件把 DOM 放進 3D 空間。顯示建築名稱、類型、高度、樓層數、地址。

資訊卡跟網站設計系統一致:半透明奶油色背景、金色邊框、襯線體標題。一個小細節,但這是「技術 demo」跟「屬於這個網站的作品」之間的差別。


Try it

👉 City Map 3D

Search any city, generate the 3D view, orbit around it, hover buildings for info, export as GLB. The whole thing runs in your browser — no server, no account, no cost.

來玩

👉 City Map 3D

搜尋任何城市、生成 3D、自由旋轉、hover 看建築資訊、匯出 GLB。全部在瀏覽器裡跑,不需要伺服器、帳號、或花任何錢。


Based on map3d by cartesiancs (MIT License). Map data © OpenStreetMap contributors.