前言
地理信息可视化一直是前端领域的热门话题。传统的 2D 地图已经无法满足我们对于视觉效果和交互体验的追求,而 3D 地图则可以提供更直观、更震撼的空间认知。本文将带你从零开始,在 Vue 项目中结合 Three.js 和 D3.js 的地理投影模块,将一个普通的 GeoJSON 文件转化为可交互的 3D 挤出地图,并支持旋转、缩放等操作。最终效果是一个具有立体感和边缘高亮的苏州各区地图。
本文所有代码均基于 Vue 3 + Three.js + d3-geo 实现,你可以直接复制代码运行体验。

原理:从经纬度到 3D 几何体
要将平面地图"立"起来,我们需要解决两个核心问题:
- 坐标转换 :地理坐标(经纬度)无法直接在 Three.js 的笛卡尔坐标系中使用。我们需要使用地图投影(如墨卡托投影)将经纬度转换为平面上的 x、y 坐标。这里我们选择 D3.js 提供的
geoMercator投影,它可以精确地将球面坐标映射到平面,并且可以通过center和scale参数将地图定位到场景中心。 - 三维挤出 :有了平面轮廓后,我们可以利用 Three.js 的
ExtrudeGeometry将平面形状挤出厚度,从而形成立体感。挤出的几何体可以赋予半透明的材质,使其看起来像一块块漂浮的玻璃板。同时,为了增强轮廓的清晰度,我们还可以在边缘绘制线条,让每个区域的分界更加明显。
整个流程可以概括为:
加载 GeoJSON → 解析几何类型(Polygon/MultiPolygon)→ 投影坐标 → 创建 Shape → 挤出 Mesh → 添加边缘 Line。
技术教程
1. 环境准备
首先创建一个 Vue 3 项目(如果你还没有),然后安装必要的依赖:
bash
npm install three d3-geo
注意:d3-geo 是 D3 的地理投影模块,我们只需要它,无需安装整个 D3。
2. 基础场景搭建
在 Vue 组件中,我们先初始化 Three.js 的核心组件:场景、相机、渲染器、轨道控制器。相机使用透视相机,并设置一个较远的初始位置(比如 z=300),以便后续加载的地图能够完整显示。
为了让画面更清晰,我们关闭阴影,限制像素比,并设置深色背景以减少视觉闪烁。
javascript
ini
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a1a);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 10000);
camera.position.set(0, 0, 300);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = false; // 关闭阴影
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.update();
3. 加载并解析 GeoJSON
GeoJSON 是一种常用的地理数据格式。我们准备了一份苏州市区的 GeoJSON 文件(可以在网上寻找或自行制作),其中包含了各区(姑苏区、虎丘区、吴中区等)的边界坐标。由于网络请求可能失败,我们添加了错误处理,并使用默认多边形作为备用。
javascript
lua
async function loadGeoJSON() {
try {
const response = await fetch('/苏州市区.geojson');
const geojson = await response.json();
processGeoJSON(geojson);
} catch (error) {
console.error('加载失败,使用默认数据', error);
const defaultGeoJSON = {
type: "FeatureCollection",
features: [{
type: "Feature",
properties: { name: "默认区域" },
geometry: {
type: "Polygon",
coordinates: [[[-10, -10], [10, -10], [10, 10], [-10, 10], [-10, -10]]]
}
}]
};
processGeoJSON(defaultGeoJSON);
}
}
4. 投影转换
在 processGeoJSON 中,我们需要遍历每一个 Feature,根据几何类型(Polygon 或 MultiPolygon)提取坐标环。使用 D3 的墨卡托投影将经纬度转换为平面坐标:
javascript
ini
import { geoMercator } from 'd3-geo';
const projection = geoMercator()
.center([120.41453, 31.342948]) // 苏州市中心经纬度
.translate([0, 0])
.scale(10000);
center 设置地图中心点,scale 控制缩放比例,translate 偏移设为 [0,0] 意味着投影后的坐标原点位于 (0,0),这样我们可以直接将坐标用于 Three.js。
5. 绘制挤出几何体
对于每一个坐标环(多边形轮廓),我们创建一个 THREE.Shape,然后通过 ExtrudeGeometry 挤出厚度。这里我们使用半透明的黄色材质,并开启一定的透明度,让内部结构隐约可见。
javascript
ini
function drawExtrudeMesh(polygon, districtName) {
const shape = new THREE.Shape();
polygon.forEach((point, index) => {
const [x, y] = projection(point);
if (index === 0) shape.moveTo(x, y);
else shape.lineTo(x, y);
});
const extrudeSettings = {
depth: 10,
bevelEnabled: false,
steps: 1
};
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
const material = new THREE.MeshBasicMaterial({
color: 'yellow',
transparent: true,
opacity: 0.5
});
return new THREE.Mesh(geometry, material);
}
注意:这里的 depth 控制挤出高度,可以根据视觉效果调整。
6. 添加边缘线条
为了区分不同区域并增强轮廓,我们在每个多边形边缘绘制一条线。线条的 z 坐标稍微抬高(比如设为 9),使其浮在挤出体的上方,避免被遮挡。
javascript
arduino
function lineEdge(polygon) {
const points = polygon.map(point => {
const [x, y] = projection(point);
return new THREE.Vector3(x, y, 9);
});
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 'yellow' });
return new THREE.Line(geometry, material);
}
7. 处理 MultiPolygon
GeoJSON 中可能存在 MultiPolygon(多个多边形构成一个区域)。我们需要递归处理,将每个子多边形分别转为 Mesh 和 Line。
javascript
ini
if (feature.geometry.type === 'MultiPolygon') {
coordinates.forEach(coordinate => {
coordinate.forEach(rows => {
map.add(drawExtrudeMesh(rows, districtName));
map.add(lineEdge(rows));
});
});
} else if (feature.geometry.type === 'Polygon') {
coordinates.forEach(rows => {
map.add(drawExtrudeMesh(rows, districtName));
map.add(lineEdge(rows));
});
}
将所有生成的物体添加到一个 THREE.Object3D(即 map)中,最后将这个组添加到场景。
8. 添加辅助和光照
为了让空间感更强,我们添加了坐标轴辅助线,并设置环境光(虽然 MeshBasicMaterial 不需要光照,但为了扩展性保留)。
javascript
ini
const axes = new THREE.AxesHelper(700);
scene.add(axes);
const light = new THREE.AmbientLight(0xffffff);
scene.add(light);
9. 启动动画循环
最后,在数据加载完成后启动动画循环,不断渲染场景。
javascript
scss
async function init() {
await loadGeoJSON();
animate();
}
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
init();
10. 完整代码整合
将上述所有片段整合到一个 Vue 组件的 <script setup> 中,即可得到一个完整的 3D 地图应用。记得将 GeoJSON 文件放置在 public 目录下。
效果预览与优化方向
运行项目后,你会看到一个悬浮在黑暗空间中的黄色半透明苏州地图,每个区都有清晰的边缘线条,你可以使用鼠标旋转、缩放查看各个角度。
下章优化点:
- 为不同区域赋予不同颜色,提高辨识度。
- 添加鼠标悬停效果,高亮当前区域并显示名称。
- 加入底图或街道标签,丰富信息层次。
- 使用
ShaderMaterial实现发光边缘等特效。 - 添加飞线效果