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

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

GitHub 地址:github.com/zhanghang20...

前言

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

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

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

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


效果预览

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


技术栈

依赖 版本 用途
Vue 3.5 组件框架,Composition API
Three.js 0.183 3D 渲染引擎
d3-geo 3.1 GeoJSON 墨卡托投影
@turf/turf 7.3 地理计算(质心、包围盒)
tinycolor2 1.6 颜色插值计算
Vite 8.x 构建工具
TypeScript 6.x 类型系统

快速开始

sh 复制代码
git clone https://github.com/zhanghang2017/threemap.git
cd threemap
npm install
npm run dev

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

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

vue 复制代码
<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>

核心架构

bash 复制代码
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 几何体。双材质数组 [顶面材质, 侧面材质] 分别控制顶面和侧面颜色。

ts 复制代码
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 中实现从底部向上移动的高亮扫光带,让地图"活起来":

glsl 复制代码
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 将两层叠加:

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

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


3. 飞线动画

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

ts 复制代码
// 贝塞尔弧线:控制点抬高形成弧形
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]:

ts 复制代码
// 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:

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

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


5. 下钻交互

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

ts 复制代码
// 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 模板。

ts 复制代码
// 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)
glow UnrealBloom 辉光强度、阈值、半径
grid 地面网格线颜色与透明度
foundation 底图装饰旋转盘(可自定义贴图)
mirror 镜面反射水面效果
wall 电子围栏高度与颜色
texture 地图顶面贴图(支持自定义图片)
autoRotate 地图自动旋转速度
emphasis hover 高亮颜色
lineStyle 省级边界线颜色
outLineStyle 地图外轮廓线颜色

Series 图层一览

ts 复制代码
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 订阅:

ts 复制代码
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

相关推荐
M ? A几秒前
Vue 转 React | VuReact编译工具快速入门
前端·javascript·vue.js·后端·react.js·面试·vureact
qq_427539833 分钟前
iframe 嵌入预览 PDF ,禁用右键菜单、打印下载按钮不展示
前端·javascript·vue.js·pdf
yu85939586 分钟前
降低OFDM系统PAPR的各种算法及误码率分析
前端·算法
ZC跨境爬虫6 分钟前
跟着 MDN 学 HTML day_3:(表单CSS美化实战与盒子模型三大核心属性详解)
前端·javascript·css·html
张风捷特烈15 分钟前
状态管理大乱斗#05 | Riverpod 源码评析 (中) - 上层建筑
android·前端·flutter
土豆125018 分钟前
Rust 生命周期开发实战:从"编译不过"到"一次过编"的实用指南
前端·rust
Rabitebla1 小时前
【C++】string 类:原理、踩坑与对象语义
linux·c语言·数据结构·c++·算法·github·学习方法
candyTong9 小时前
一觉醒来,大模型就帮我排查完页面性能问题
前端·javascript·架构
魔术师Grace9 小时前
我给 AI 做了场入职培训
前端·程序员