用 Three.js + Vue 3 打造炫酷的 3D 行政地图可视化组件

0 阅读8分钟

用 Three.js + Vue 3 打造炫酷的 3D 行政地图可视化组件

GitHub 地址:github.com/zhanghang20…

前言

在大屏数据可视化场景中,地图是最常见也最重要的展示载体。ECharts 的平面地图虽然成熟,但在追求视觉冲击力的场合——比如指挥大屏、智慧城市驾驶舱——平铺的二维地图已经难以满足需求。

于是我用 Three.js + Vue 3 从零手写了一个 3D 地理可视化组件 ThreeMap,支持:

  • GeoJSON 驱动的行政区域 3D 拉伸
  • UnrealBloom 选择性辉光后处理
  • 多种数据图层(Marker / 扩散点 / 飞线 / 柱状图 / 棱柱图)
  • 双击省份下钻交互
  • 侧面扫光 Shader 动画
  • 镜面反射 / 电子围栏 / 底图旋转装饰等

本文记录这个组件的设计思路和核心实现,并开放全部源码。


效果预览

image.png

3D 拉伸地图 + 辉光边界 + Marker/飞线/柱状图层 + 可视化控制面板


技术栈

依赖版本用途
Vue3.5组件框架,Composition API
Three.js0.1833D 渲染引擎
d3-geo3.1GeoJSON 墨卡托投影
@turf/turf7.3地理计算(质心、包围盒)
tinycolor21.6颜色插值计算
Vite8.x构建工具
TypeScript6.x类型系统

快速开始

git clone https://github.com/zhanghang2017/threemap.git
cd threemap
npm install
npm run dev

直接使用 ThreeMap 类(自定义集成)

如果需要自行控制数据、事件和下钻逻辑,可以使用底层的 ThreeMap.ts 类:

<template>
  <div ref="mapRef" style="width: 100%; height: 600px" />
  <div ref="tooltipRef" />
</template>

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
import ThreeMap from '@/components/ThreeMap/ThreeMap'
import createDefaultOptions from '@/components/ThreeMap/options/threeOption'

const mapRef = ref<HTMLDivElement | null>(null)
const tooltipRef = ref<HTMLDivElement | null>(null)
let map: ThreeMap | null = null

onMounted(async () => {
  map = new ThreeMap(mapRef.value!, tooltipRef.value!)

  const options = createDefaultOptions()

  // 注册并加载地图 GeoJSON
  const mapJson = await map.registerMap('100000', '100000', options.config)

  // 监听事件
  map.on('click', (data) => {
    console.log('点击区域:', data.name, data.adcode)
  })
  map.on('dblclick', (data) => {
    console.log('双击下钻:', data.name)
  })

  // 渲染
  map.setOption({ ...options, map: '100000' })
})

onBeforeUnmount(() => {
  map?.destroyMap()
  map = null
})
</script>

核心架构

ThreeMap/
├── ThreeMap.ts          # 核心类:Three.js 渲染引擎封装
├── index.vue            # Vue 组件包装层(含下钻、控制面板)
├── types.ts             # 完整 TypeScript 类型定义
├── options/             # 配置工厂函数
│   ├── threeOption.ts   # createDefaultOptions()
│   ├── createMarker.ts
│   ├── createFlight.ts
│   ├── createCylinder.ts
│   ├── createPrism.ts
│   └── createScatter.ts
└── utils/               # 各图层绘制函数
    ├── drawDistrict.ts  # 地图拉伸(核心)
    ├── drawGlow.ts      # 辉光后处理
    ├── drawFlight.ts    # 飞线动画
    ├── drawCylinder.ts  # 圆柱图层
    ├── drawScatter.ts   # 扩散点
    ├── drawMarker.ts    # 标记点(CSS2D)
    ├── drawPrism.ts     # 棱柱图层
    ├── drawPlane.ts     # 镜面/底色平面
    ├── drawGrid.ts      # 网格线
    ├── drawFoundation.ts# 底图装饰盘
    ├── drawOutLine.ts   # 轮廓线
    ├── addTooltip.ts    # Tooltip 管理
    └── helpers.ts       # 颜色插值、坐标工具

整体采用职责分离的设计:ThreeMap.ts 负责 Three.js 生命周期和事件系统,各 draw* 工具函数只负责生成并返回 THREE.Group,组合方式灵活。


核心实现解析

1. GeoJSON → 3D 拉伸地图

地图渲染的核心是把 GeoJSON 的经纬度坐标转换为 Three.js 的 3D 网格。

投影转换:用 d3-geogeoMercator 将经纬度映射到屏幕坐标,再配合地图宽高自动计算缩放比例(autoScale 模式)。

拉伸几何体:用 THREE.Shape 描绘行政区边界的 2D 轮廓,再用 THREE.ExtrudeGeometry 沿 Z 轴拉伸成带厚度的 3D 几何体。双材质数组 [顶面材质, 侧面材质] 分别控制顶面和侧面颜色。

const geometry = new THREE.ExtrudeGeometry(shape, {
  depth: options.config.depth,
  bevelEnabled: false,
})
// 双材质:[0] 顶面,[1] 侧面
const mesh = new THREE.Mesh(geometry, [topMaterial, sideMaterial])
mesh.rotation.x = -0.5 * Math.PI // XY 平面旋转到 XZ 平面

侧面扫光动画:通过 onBeforeCompile 钩子注入自定义 GLSL,在侧面材质的 fragmentShader 中实现从底部向上移动的高亮扫光带,让地图"活起来":

float y = uStart + uTime * uSpeed;
float h = uHeight;
if(vPosition.z > y && vPosition.z < y + h) {
  float per = (vPosition.z - y) / h;
  outgoingLight = mix(outgoingLight, uColor, per);
}

2. 选择性辉光后处理

Three.js UnrealBloomPass 的经典难题:辉光会"污染"不需要发光的对象(文字、图标变模糊)。

解决方案是双 Composer 架构

bloomComposer
  └── RenderPass(隐藏非辉光对象)
  └── UnrealBloomPass(只渲染辉光层)

finalComposer
  └── RenderPass(渲染完整场景)
  └── ShaderPass(将 bloom 纹理叠加到基础渲染)
  └── SMAAPass(抗锯齿)

渲染时遍历场景,将未标记 _isGlow = true 的对象临时隐藏,bloomComposer 渲染完毕后再恢复,最终 ShaderPass 将两层叠加:

// 混合 Fragment Shader
gl_FragColor = vec4(
  base_color.rgb + bloom_color.rgb,
  max(base_color.a, lum)
);

边界线、电子围栏等对象标记 _isGlow = true,可以获得辉光效果,而文字标签和图标则不受影响。


3. 飞线动画

飞线用 THREE.Points(粒子系统)而非 Line,每条飞线是一组沿贝塞尔曲线均匀分布的粒子,通过自定义 ShaderMaterial 实现头部粒子大、尾部渐隐的拖尾效果。

// 贝塞尔弧线:控制点抬高形成弧形
const controlPoint = new THREE.Vector3(
  (start.x + end.x) / 2,
  (start.y + end.y) / 2,
  arcHeight, // 弧度高度
)
const curve = new THREE.QuadraticBezierCurve3(startVec, controlPoint, endVec)
const points = curve.getPoints(COUNT) // 采样粒子坐标

每帧动画中,每个飞线 Mesh 的 time uniform 递增,Shader 根据粒子在线段中的相对位置和 time 计算头部粒子范围,超出范围后重置(形成循环动画)。

支持多色渐变飞头:通过 flightColor 数组分别为多个飞线实例设置颜色,达到"多彩飞线"效果。


4. 数据可视化色阶映射

区域颜色支持两种映射模式:

range 连续渐变模式:将 value 在 [min, max] 范围内线性插值,从 color[0] 渐变到 color[1]:

// tinycolor2 + 自定义插值
export const rangeColor = (value: number, min: number, max: number, colors: string[]) => {
  const ratio = (value - min) / (max - min)
  // 在色数组中找到对应区间进行混合
  return interpolateColor(colors, ratio)
}

separate 分段规则模式:按阈值匹配 rules 数组,类似 ECharts 的 visualMap piecewise:

rules: [
  { value: 0, color: '#1A3A6B', label: '低' },
  { value: 50, color: '#00BFFF', label: '中' },
  { value: 80, color: '#00FFFF', label: '高' },
]

同时内置 visualMap 可视化映射条,支持渐变色条(range 模式)和分段图例(separate 模式),悬浮在地图容器上,样式完全可配置。


5. 下钻交互

双击区域触发下钻,核心是 registerMap + setOption 的组合:

// 1. 加载该省/市的 GeoJSON 文件
const mapJson = await map.registerMap(adcode, adcode, config)

// 2. 重新渲染
map.setOption({
  ...currentOptions,
  map: adcode, // 切换到新地图
  data: newData,
})

registerMap 会用 d3-geo 重新计算投影中心和缩放比例,调整相机位置,完成地图级别的切换。支持从全国 → 省级 → 市级逐层下钻,并提供返回按钮还原上一级。


6. CSS2D 标签 / Tooltip

使用 Three.js 的 CSS2DRenderer 将 HTML DOM 元素定位到 3D 对象的世界坐标上,解决纯 Canvas 文字渲染不清晰的问题。

Tooltip 则基于 Raycaster 射线检测,mousemove 时计算鼠标与场景对象的交叉点,命中后更新 tooltip DOM 的 left/top 坐标并渲染自定义 HTML 模板。

// formatter 支持返回任意 HTML 字符串
tooltip: {
  formatter: (data) => `
    <div class="tooltip-title">${data.name}</div>
    <div class="tooltip-value">数值:${data.value}</div>
  `
}

完整配置项速览

配置项说明
config地图基础:缩放、深度、禁用旋转/缩放
camera初始相机位置 (x/y/z)
itemStyle顶面/侧面颜色、数据色阶映射
label区域文字标注(支持自定义 HTML formatter)
tooltip悬浮提示框(支持自定义 HTML formatter)
glowUnrealBloom 辉光强度、阈值、半径
grid地面网格线颜色与透明度
foundation底图装饰旋转盘(可自定义贴图)
mirror镜面反射水面效果
wall电子围栏高度与颜色
texture地图顶面贴图(支持自定义图片)
autoRotate地图自动旋转速度
emphasishover 高亮颜色
lineStyle省级边界线颜色
outLineStyle地图外轮廓线颜色

Series 图层一览

series: [
  // 标记点(SVG/图片图标)
  { type: 'marker', symbol: '<svg>...</svg>', symbolSize: [30, 30], data: [...] },

  // 扩散点(呼吸动画)
  { type: 'scatter', spotColor: '#00FFFF', ringRatio: 3, data: [...] },

  // 渐变圆柱 / 发光塔
  { type: 'cylinder', mode: 'cylinder', color: ['#00BFFF', '#00FFFF'], maxHeight: 3, data: [...] },

  // 三/四/六棱柱
  { type: 'prism', prismType: 6, size: 0.15, maxHeight: 3, data: [...] },

  // 飞线动画
  { type: 'flight', speed: 0.003, flightColor: ['#00FFFF', '#0080FF'], data: [
    { start: '110000', end: '310000' }, // 支持 adcode 或经纬度
  ]},
]

事件 API

事件系统由 ThreeMap.ts 类提供,index.vue 已内部消费(下钻逻辑在组件内部处理)。直接使用 ThreeMap 类时,通过 on / off 订阅:

const map = new ThreeMap(containerEl, tooltipEl)

map.on('click', (data) => {
  // data: { name, adcode, value, ... }
  console.log(data.name, data.adcode)
})
map.on('dblclick', (data) => {
  // 手动实现下钻(调用 registerMap + setOption)
})
map.on('change', (camera) => {
  // 相机位置变化(可用于记录视角)
})

map.off('click') // 移除指定事件的所有监听
map.off() // 移除所有事件监听

设计亮点总结

  1. 纯 Options 驱动:所有配置通过一个 ThreeMapOptions 对象传入,setOption 单次调用完成全量重渲染,API 风格接近 ECharts,对业务层友好。

  2. 选择性辉光:双 Composer 架构解决了 Three.js 辉光"污染"全场景的痛点,边界线发光而文字清晰。

  3. Shader 注入扫光:通过 onBeforeCompile 而非自定义 ShaderMaterial,在保留 Three.js 内置光照计算的同时注入扫光动画逻辑。

  4. CSS2D + Canvas 混合渲染:图标/标签用 CSS2DRenderer 渲染,保证文字清晰;数据图层(圆柱、飞线)用 WebGL 渲染,保证动画性能。

  5. ResizeObserver 自适应:容器尺寸变化时自动重新计算地图缩放和相机参数,无需手动调用 resize。


浏览器兼容

需要支持 WebGL 2 的现代浏览器(Chrome 80+、Firefox 79+、Edge 80+),不支持 IE。


结语

ThreeMap 是我在项目中积累的结晶。如果你正在做数据可视化大屏,欢迎 Star 或提 Issue。

如果觉得对你有帮助,给个 ⭐ 是最大的鼓励!

GitHub:github.com/zhanghang20…


技术栈:Vue 3 / Three.js / TypeScript / d3-geo / Vite