Three.js + D3.js 实战:从零构建高交互性3D中国地图可视化系统
最近在做一个数据大屏项目,客户要求将全国的业务数据以三维地图的形式直观呈现,并且要能点击、能下钻、有动画。市面上现成的图表库要么效果太“平”,要么定制化程度不够。折腾了一圈,最终决定用 Three.js 和 D3.js 自己动手搭一套。这个组合听起来有点硬核,但实际用下来,你会发现它给的可控性和表现力是其他方案难以比拟的。
这篇文章,我就把自己从踩坑到实现的全过程梳理出来,重点不是给你一段能直接拷贝的代码,而是帮你理解背后的原理和设计思路。无论你是想做一个酷炫的数据看板,还是想在产品里加入地理维度的3D分析,相信这些实战经验都能给你带来启发。我们会从最基础的数据获取和坐标转换讲起,一步步实现地图渲染、交互高亮、视角聚焦,甚至飞线和光柱这些高级特效。准备好了吗?我们开始。
1. 项目基石:数据、坐标与三维场景搭建
任何地图可视化的起点都是数据。对于中国地图,我们需要一份包含各省份边界坐标的 GeoJSON 数据。这类数据可以从一些公开的地理信息平台获取。拿到数据后,第一个要解决的问题是:如何将经纬度坐标转换成 Three.js 能理解的平面直角坐标?
1.1 GeoJSON 数据与墨卡托投影
GeoJSON 数据中的 coordinates 字段存储的是经纬度数组,例如 [116.405285, 39.904989] 代表北京。Three.js 的三维空间使用的是笛卡尔坐标系(x, y, z),我们需要一个转换函数。这就是 D3.js 的 d3-geo 模块大显身手的地方,它提供了成熟的墨卡托投影方法。
import * as d3 from 'd3';
// 创建墨卡托投影转换函数
const projection = d3.geoMercator()
.center([104.0, 37.5]) // 以中国地理中心为投影中心
.scale(120) // 缩放系数,决定地图大小
.translate([0, 0]); // 平移,通常先设为[0,0],后续调整
// 使用:将经纬度 [116.405, 39.905] 转换为 Three.js 中的 (x, y)
const [x, y] = projection([116.405285, 39.904989]);
// 注意:转换后的y轴方向与Three.js默认可能相反,有时需要取负值 (-y)
这里有几个参数需要根据你的画布大小和需求调整:
center: 投影的中心点经纬度,设为中国的中心,能让地图在视觉上更居中。scale: 这是最关键的一个参数。值太小,地图会缩在屏幕一角;值太大,地图会超出画布。需要反复调试。translate: 通常与scale配合使用,用于将投影后的地图平移到场景中心。
提示:调试阶段,可以创建一个简单的点(如
THREE.SphereGeometry)放在投影后的坐标上,快速验证你的projection函数是否正确,以及scale是否合适。
1.2 初始化 Three.js 基础环境
在转换坐标之前,我们需要先搭建好 Three.js 的“舞台”。这包括场景、相机、渲染器和控制器。
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
// 1. 场景 - 所有3D对象的容器
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a1a2a); // 设置一个深蓝色背景
// 2. 相机 - 观察场景的视角
const camera = new THREE.PerspectiveCamera(
60, // 视野角度 (FOV)
window.innerWidth / window.innerHeight, // 宽高比
0.1, // 近裁剪面
2000 // 远裁剪面
);
camera.position.set(0, -100, 150); // 初始相机位置,俯视角度
// 3. 渲染器 - 将3D场景绘制到HTML Canvas上
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio); // 适配高清屏
document.body.appendChild(renderer.domElement);
// 4. 轨道控制器 - 允许用户用鼠标拖拽、缩放、旋转地图
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // 启用阻尼,产生平滑的交互效果
controls.dampingFactor = 0.05;
// 5. 光源 - 没有光,3D物体将是全黑的
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); // 环境光,均匀照亮
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); // 方向光,产生阴影和立体感
directionalLight.position.set(100, 100, 50);
scene.add(directionalLight);
// 6. 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update(); // 必须在动画循环中更新控制器
renderer.render(scene, camera);
}
animate();
// 7. 响应窗口大小变化
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.inner

2618

被折叠的 条评论
为什么被折叠?



