用 Three.js 和 D3 在 Vue 中打造 3D 苏州地图

前言

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

本文所有代码均基于 Vue 3 + Three.js + d3-geo 实现,你可以直接复制代码运行体验。

原理:从经纬度到 3D 几何体

要将平面地图"立"起来,我们需要解决两个核心问题:

  1. 坐标转换 :地理坐标(经纬度)无法直接在 Three.js 的笛卡尔坐标系中使用。我们需要使用地图投影(如墨卡托投影)将经纬度转换为平面上的 x、y 坐标。这里我们选择 D3.js 提供的 geoMercator 投影,它可以精确地将球面坐标映射到平面,并且可以通过 centerscale 参数将地图定位到场景中心。
  2. 三维挤出 :有了平面轮廓后,我们可以利用 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 实现发光边缘等特效。
  • 添加飞线效果
相关推荐
叶智辽1 天前
Three.js多视口渲染:如何在一个屏幕上同时展示三个视角
webgl·three.js·数据可视化
叶智辽6 天前
【Three.js与WebGPU】下一代3D技术到底强在哪?
webgl·three.js
叶智辽6 天前
【Three.js后期处理】如何让你的场景拥有电影级调色
前端·three.js
叶智辽7 天前
【Three.js多相机渲染】如何在同一场景里实现“画中画”效果
three.js·canvas
答案answer7 天前
一个非常实用的Three.js3D模型爆破💥和切割开源插件
前端·github·three.js
叶智辽8 天前
【Three.js内存管理】那些你以为释放了,其实还在占着的资源
性能优化·three.js
烛阴9 天前
Three.js 零基础入门:手把手打造交互式 3D 几何体展示系统
javascript·webgl·three.js
叶智辽10 天前
【ThreeJS调试技巧】那些让 Bug 无所遁形的“脏套路”
webgl·three.js
叶智辽11 天前
【ThreeJS急诊室】一个生产事故:我把客户的工厂渲染“透明”了
webgl·three.js