学习Three.js--基于GeoJSON绘制2D矢量地图
前置核心说明
开发目标
基于Three.js实现纯矢量2D地图,核心能力包括:
- 将GeoJSON地理数据(经纬度)转换为Three.js可渲染的平面图形;
- 支持墨卡托投影(经纬度→平面坐标)、地图居中缩放适配视口;
- 实现鼠标点击省份高亮(填充+边框变色);
- 纯2D交互(仅平移/缩放,禁用旋转),模拟传统地图体验。
- 开发效果如下

核心技术栈
| 技术点 |
作用 |
OrthographicCamera(正交相机) |
实现无透视的2D效果(无近大远小),是2D地图的核心相机类型 |
| 墨卡托投影函数 |
将地理经纬度(lon/lat)转换为平面笛卡尔坐标(x/y) |
Shape/ShapeGeometry
|
将GeoJSON的多边形坐标转换为Three.js可渲染的几何形状 |
Raycaster(射线检测) |
实现鼠标点击与省份图形的交互(命中检测) |
OrbitControls(轨道控制器) |
自定义交互规则(禁用旋转,仅保留平移/缩放) |
| GeoJSON |
行业标准地理数据格式,存储省份的多边形坐标信息 |
分步开发详解
步骤1:基础环境搭建(场景/相机/渲染器/控制器)
1.1 核心代码
// 存储所有省份Mesh,用于射线检测
const provinceMeshes = [];
// 1. 场景初始化(地图容器)
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf8fafc); // 浅灰背景
// 2. 正交相机(核心!2D地图必须用正交相机)
const aspect = window.innerWidth / window.innerHeight; // 窗口宽高比
const frustumSize = 800; // 相机视口高度(控制地图初始显示范围)
const camera = new THREE.OrthographicCamera(
-frustumSize * aspect / 2, // 左边界
frustumSize * aspect / 2, // 右边界
frustumSize / 2, // 上边界
-frustumSize / 2, // 下边界
1, // 近裁切面(最近可见距离)
1000 // 远裁切面(最远可见距离)
);
camera.position.set(0, 0, 10); // 相机在Z轴,看向场景中心
camera.lookAt(0, 0, 0);
// 3. 渲染器(抗锯齿+高清适配)
const renderer = new THREE.WebGLRenderer({ antialias: 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.enableRotate = false; // 禁用旋转(纯2D地图)
controls.enablePan = true; // 启用平移(拖拽地图)
controls.enableZoom = true; // 启用缩放(滚轮)
controls.zoomSpeed = 1.5; // 缩放速度(适配体验)
1.2 关键参数解析
-
正交相机参数:区别于透视相机(PerspectiveCamera),正交相机的视口是矩形,所有物体无论距离远近大小一致,完美适配2D地图;
-
frustumSize:控制相机视口高度,值越大地图初始显示范围越大;
-
控制器配置:禁用旋转是2D地图的核心要求,避免视角倾斜。
步骤2:墨卡托投影函数(经纬度→平面坐标)
2.1 核心代码
// 墨卡托投影:将经纬度(lon/lat)转换为平面坐标(x/y)
function mercator(lon, lat) {
const R = 6378137; // WGS84坐标系地球半径(米)
const x = (lon * Math.PI / 180) * R; // 经度转弧度 × 地球半径
const y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) * R; // 纬度投影公式
return { x, y }; // y轴北为正,x轴东为正
}
2.2 原理说明
- 墨卡托投影是地图领域的标准投影方式,将球形地球的经纬度转换为平面矩形坐标;
- 经度(lon)范围:-180°
180°,纬度(lat)范围:-90°90°;
- 核心公式:
- 经度转换:直接将角度转为弧度后乘以地球半径;
- 纬度转换:通过正切+对数函数,解决纬度越靠近极点拉伸越大的问题。
步骤3:GeoJSON加载与全局边界计算
3.1 核心代码
let bounds = { minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity };
// 异步加载GeoJSON并绘制地图
async function loadAndDrawMap() {
// 1. 加载GeoJSON数据(本地文件)
const response = await fetch('./china.json');
const geojson = await response.json();
// 2. 手动校正港澳位置(墨卡托投影后的偏移,单位:米)
const SPECIAL_REGION_OFFSETS = {
'香港特别行政区': { dx: 80000, dy: -60000 },
'澳门特别行政区': { dx: 100000, dy: -80000 }
};
// 3. 遍历所有坐标,计算全局边界(用于地图居中缩放)
geojson.features.forEach(feature => {
traverseCoordinates(feature.geometry.coordinates, (lon, lat) => {
const { x, y } = mercator(lon, lat);
// 更新边界值(最小/最大x/y)
bounds.minX = Math.min(bounds.minX, x);
bounds.maxX = Math.max(bounds.maxX, x);
bounds.minY = Math.min(bounds.minY, y);
bounds.maxY = Math.max(bounds.maxY, y);
});
});
// 4. 计算地图中心和缩放比例(适配视口)
const centerX = (bounds.minX + bounds.maxX) / 2; // 地图中心X
const centerY = (bounds.minY + bounds.maxY) / 2; // 地图中心Y
const width = bounds.maxX - bounds.minX; // 地图宽度
const height = bounds.maxY - bounds.minY; // 地图高度
const scale = 700 / Math.max(width, height); // 缩放比例(使地图适配视口)
// 后续创建省份Shape...
}
// 递归遍历GeoJSON坐标(处理Polygon/MultiPolygon嵌套结构)
function traverseCoordinates(coords, callback) {
if (typeof coords[0] === 'number') {
// 基础情况:[lon, lat] 数组,执行回调
callback(coords[0], coords[1]);
} else {
// 递归情况:嵌套数组,继续遍历
coords.forEach(c => traverseCoordinates(c, callback));
}
}
3.2 关键逻辑解析
-
边界计算:遍历所有省份的所有坐标,得到地图的最小/最大x/y,用于后续居中;
-
港澳偏移:解决GeoJSON中港澳坐标投影后位置偏差的问题;
-
缩放比例:
700 / Math.max(width, height) 保证地图的最大维度适配视口(700为经验值,可调整);
-
递归遍历坐标:GeoJSON的
Polygon是单层数组,MultiPolygon是双层数组,需递归处理所有嵌套坐标。
步骤4:创建Shape与省份Mesh
4.1 核心代码
// 在loadAndDrawMap函数内,边界计算后执行:
geojson.features.forEach(feature => {
const shapes = [];
const provinceName = feature.properties.name;
const offset = SPECIAL_REGION_OFFSETS[provinceName] || { dx: 0, dy: 0 };
// 处理单个Polygon(如大多数省份)
if (feature.geometry.type === 'Polygon') {
const shape = createShape(feature.geometry.coordinates[0], centerX, centerY, scale, offset);
if (shape) shapes.push(shape);
}
// 处理MultiPolygon(如包含岛屿的省份:海南、浙江等)
else if (feature.geometry.type === 'MultiPolygon') {
feature.geometry.coordinates.forEach(polygon => {
const shape = createShape(polygon[0], centerX, centerY, scale, offset);
if (shape) shapes.push(shape);
});
}
// 为每个Shape创建Mesh(填充)和Line(边框)
shapes.forEach(shape => {
// 1. 创建填充几何体
const geometry = new THREE.ShapeGeometry(shape);
// 2. 填充材质(蓝色,双面渲染)
const material = new THREE.MeshBasicMaterial({
color: 0x3b82f6,
side: THREE.DoubleSide
});
// 3. 创建省份Mesh
const mesh = new THREE.Mesh(geometry, material);
// 关键:绑定省份信息到userData(交互时使用)
mesh.userData = {
provinceName: feature.properties.name,
originalColor: 0x3b82f6,
isHighlighted: false
};
scene.add(mesh);
provinceMeshes.push(mesh); // 加入数组,用于射线检测
// 4. 创建白色边框
const borderGeo = new THREE.BufferGeometry().setFromPoints(shape.getPoints());
const borderMat = new THREE.LineBasicMaterial({ color: 0xffffff, linewidth: 2 });
const border = new THREE.Line(borderGeo, borderMat);
mesh.border = border; // 边框绑定到Mesh,方便后续修改颜色
scene.add(border);
});
});
// 创建Shape:将GeoJSON多边形转换为Three.js Shape
function createShape(ring, centerX, centerY, scale, offset = { dx: 0, dy: 0 }) {
if (ring.length < 3) return null; // 少于3个点无法构成多边形
const shape = new THREE.Shape();
// 转换所有点为平面坐标,并应用居中+缩放+偏移
const points = ring.map(([lon, lat]) => {
const { x, y } = mercator(lon, lat);
const shiftedX = x + offset.dx;
const shiftedY = y + offset.dy;
return {
x: (shiftedX - centerX) * scale, // 居中后缩放
y: (shiftedY - centerY) * scale
};
});
// 绘制Shape:移动到第一个点,依次连线
shape.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
shape.lineTo(points[i].x, points[i].y);
}
return shape;
}
4.2 关键逻辑解析
-
Shape创建:
THREE.Shape是Three.js的2D形状对象,通过moveTo+lineTo绘制多边形;
-
MultiPolygon处理:包含多个多边形的省份(如海南=海南岛+南海诸岛),需为每个多边形创建独立Shape;
-
userData绑定:Three.js的Object3D对象可通过
userData存储自定义数据,这里绑定省份名称/原始颜色,是交互的核心;
-
边框创建:通过
shape.getPoints()获取Shape的顶点,创建Line实现边框效果。
步骤5:Raycaster射线检测(鼠标点击高亮)
5.1 核心代码
let highlightedProvince = null; // 记录当前高亮的省份
// 鼠标点击事件处理
window.addEventListener('click', onDocumentMouseDown, false);
function onDocumentMouseDown(event) {
// 1. 将鼠标屏幕坐标转换为NDC(归一化设备坐标,范围-1~1)
const mouse = new THREE.Vector2();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
// 2. 创建Raycaster(射线检测)
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera); // 设置射线:从相机到鼠标位置
// 3. 检测射线与所有省份Mesh的相交
const intersects = raycaster.intersectObjects(provinceMeshes);
if (intersects.length > 0) {
// 点击到省份:切换高亮
const clickedMesh = intersects[0].object;
// 取消之前的高亮
if (highlightedProvince) {
highlightedProvince.material.color.set(highlightedProvince.userData.originalColor);
if (highlightedProvince.border) {
highlightedProvince.border.material.color.set(0xffffff);
}
highlightedProvince.userData.isHighlighted = false;
}
// 高亮当前点击的省份
clickedMesh.material.color.set(0xffd700); // 金色填充
if (clickedMesh.border) {
clickedMesh.border.material.color.set(0xff0000); // 红色边框
}
clickedMesh.userData.isHighlighted = true;
highlightedProvince = clickedMesh;
console.log('点击了:', clickedMesh.userData.provinceName);
} else {
// 点击空白处:取消所有高亮
if (highlightedProvince) {
highlightedProvince.material.color.set(highlightedProvince.userData.originalColor);
if (highlightedProvince.border) {
highlightedProvince.border.material.color.set(0xffffff);
}
highlightedProvince.userData.isHighlighted = false;
highlightedProvince = null;
}
}
}
5.2 射线检测核心原理
-
NDC坐标转换:屏幕坐标(clientX/clientY)转换为归一化设备坐标(-1~1),是Raycaster的标准输入;
-
射线创建:
raycaster.setFromCamera(mouse, camera) 生成从相机位置指向鼠标位置的射线;
-
相交检测:
raycaster.intersectObjects(provinceMeshes) 返回射线与Mesh的相交结果,优先返回最近的Mesh;
-
高亮逻辑:通过修改材质颜色实现高亮,利用
userData存储原始颜色,保证切换回退。
步骤6:窗口适配与渲染循环
6.1 核心代码
// 窗口大小适配
window.addEventListener('resize', () => {
const aspect = window.innerWidth / window.innerHeight;
const frustumSize = 800;
// 更新正交相机边界
camera.left = -frustumSize * aspect / 2;
camera.right = frustumSize * aspect / 2;
camera.top = frustumSize / 2;
camera.bottom = -frustumSize / 2;
camera.updateProjectionMatrix(); // 必须更新投影矩阵
renderer.setSize(window.innerWidth, window.innerHeight); // 更新渲染器尺寸
});
// 渲染循环(持续渲染场景)
function animate() {
requestAnimationFrame(animate); // 浏览器刷新率同步
controls.update(); // 更新控制器(阻尼/平移/缩放)
renderer.render(scene, camera); // 渲染场景
}
animate();
// 启动地图加载
loadAndDrawMap().catch(err => console.error('加载失败:', err));
6.2 关键注意点
-
投影矩阵更新:正交相机参数修改后,必须调用
camera.updateProjectionMatrix()使修改生效;
-
渲染循环:Three.js需要持续调用
renderer.render()才能显示画面,requestAnimationFrame保证帧率稳定。
核心方法/参数速查表
1. 核心类/函数参数
| 类/函数 |
关键参数 |
说明 |
OrthographicCamera |
left/right/top/bottom/near/far |
正交相机边界,near/far控制可见距离 |
mercator(lon, lat) |
lon(经度)、lat(纬度) |
返回{x,y}平面坐标,R=6378137(地球半径) |
traverseCoordinates(coords, callback) |
coords(GeoJSON坐标)、callback(遍历回调) |
递归处理嵌套坐标,回调参数为lon/lat |
createShape(ring, centerX, centerY, scale, offset) |
ring(多边形坐标环)、centerX/Y(地图中心)、scale(缩放)、offset(偏移) |
返回THREE.Shape,实现坐标居中+缩放 |
Raycaster.setFromCamera(mouse, camera) |
mouse(NDC坐标)、camera(相机) |
创建从相机到鼠标的射线 |
2. 交互核心参数
| 参数 |
作用 |
mesh.userData |
存储省份名称/原始颜色/高亮状态,交互时读取 |
intersects[0].object |
射线检测命中的第一个Mesh(最近的省份) |
controls.enableRotate |
禁用旋转(false),保证2D地图体验 |
完整优化代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>Three.js 中国地图 - 2D矢量版</title>
<style>
body {
margin: 0;
overflow: hidden;
background: #f8fafc;
}
/* 可选:添加加载提示 */
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 18px;
color: #666;
}
</style>
</head>
<body>
<div class="loading">加载地图中...</div>
<script type="module">
import * as THREE from 'https://esm.sh/three@0.174.0';
import { OrbitControls } from 'https://esm.sh/three@0.174.0/examples/jsm/controls/OrbitControls.js';
// 全局变量:存储所有省份Mesh(用于射线检测)
const provinceMeshes = [];
// 全局变量:记录当前高亮的省份
let highlightedProvince = null;
// 全局变量:地图边界(用于居中缩放)
let bounds = { minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity };
// ========== 1. 初始化基础环境 ==========
// 场景:所有元素的容器
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf8fafc); // 浅灰背景
// 正交相机:2D地图核心(无透视)
const aspect = window.innerWidth / window.innerHeight;
const frustumSize = 800; // 相机视口高度(控制初始显示范围)
const camera = new THREE.OrthographicCamera(
-frustumSize * aspect / 2,
frustumSize * aspect / 2,
frustumSize / 2,
-frustumSize / 2,
1, 1000
);
camera.position.set(0, 0, 10); // 相机在Z轴,看向场景中心
camera.lookAt(0, 0, 0);
// 渲染器:抗锯齿+高清适配
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
// 轨道控制器:自定义2D交互规则
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableRotate = false; // 禁用旋转(纯2D)
controls.enablePan = true; // 启用平移(拖拽)
controls.enableZoom = true; // 启用缩放(滚轮)
controls.zoomSpeed = 1.5; // 缩放速度适配
// ========== 2. 墨卡托投影函数 ==========
/**
* 墨卡托投影:经纬度转平面坐标
* @param {Number} lon - 经度(-180~180)
* @param {Number} lat - 纬度(-90~90)
* @returns {Object} {x, y} 平面坐标(米)
*/
function mercator(lon, lat) {
const R = 6378137; // WGS84地球半径
const x = (lon * Math.PI / 180) * R;
const y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) * R;
return { x, y };
}
// ========== 3. GeoJSON加载与绘制 ==========
/**
* 递归遍历GeoJSON坐标(处理Polygon/MultiPolygon嵌套)
* @param {Array} coords - GeoJSON坐标数组
* @param {Function} callback - 遍历回调(lon, lat)
*/
function traverseCoordinates(coords, callback) {
if (typeof coords[0] === 'number') {
callback(coords[0], coords[1]);
} else {
coords.forEach(c => traverseCoordinates(c, callback));
}
}
/**
* 创建Three.js Shape(多边形)
* @param {Array} ring - 单个多边形坐标环
* @param {Number} centerX - 地图中心X
* @param {Number} centerY - 地图中心Y
* @param {Number} scale - 缩放比例
* @param {Object} offset - 位置偏移(dx, dy)
* @returns {THREE.Shape|null} 形状对象
*/
function createShape(ring, centerX, centerY, scale, offset = { dx: 0, dy: 0 }) {
if (ring.length < 3) return null; // 最少3个点构成多边形
const shape = new THREE.Shape();
const points = ring.map(([lon, lat]) => {
const { x, y } = mercator(lon, lat);
const shiftedX = x + offset.dx;
const shiftedY = y + offset.dy;
// 居中+缩放:转换为相机视口坐标
return {
x: (shiftedX - centerX) * scale,
y: (shiftedY - centerY) * scale
};
});
// 绘制Shape
shape.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
shape.lineTo(points[i].x, points[i].y);
}
return shape;
}
/**
* 加载GeoJSON并绘制地图
*/
async function loadAndDrawMap() {
try {
// 1. 加载GeoJSON数据
const response = await fetch('./china.json');
const geojson = await response.json();
// 2. 港澳位置偏移(校正投影偏差)
const SPECIAL_REGION_OFFSETS = {
'香港特别行政区': { dx: 80000, dy: -60000 },
'澳门特别行政区': { dx: 100000, dy: -80000 }
};
// 3. 遍历所有坐标,计算全局边界
geojson.features.forEach(feature => {
traverseCoordinates(feature.geometry.coordinates, (lon, lat) => {
const { x, y } = mercator(lon, lat);
bounds.minX = Math.min(bounds.minX, x);
bounds.maxX = Math.max(bounds.maxX, x);
bounds.minY = Math.min(bounds.minY, y);
bounds.maxY = Math.max(bounds.maxY, y);
});
});
// 4. 计算地图中心和缩放比例
const centerX = (bounds.minX + bounds.maxX) / 2;
const centerY = (bounds.minY + bounds.maxY) / 2;
const width = bounds.maxX - bounds.minX;
const height = bounds.maxY - bounds.minY;
const scale = 700 / Math.max(width, height); // 适配视口
// 5. 遍历每个省份,创建Shape和Mesh
geojson.features.forEach(feature => {
const shapes = [];
const provinceName = feature.properties.name;
const offset = SPECIAL_REGION_OFFSETS[provinceName] || { dx: 0, dy: 0 };
// 处理Polygon(单多边形)
if (feature.geometry.type === 'Polygon') {
const shape = createShape(feature.geometry.coordinates[0], centerX, centerY, scale, offset);
if (shape) shapes.push(shape);
}
// 处理MultiPolygon(多多边形,如海南)
else if (feature.geometry.type === 'MultiPolygon') {
feature.geometry.coordinates.forEach(polygon => {
const shape = createShape(polygon[0], centerX, centerY, scale, offset);
if (shape) shapes.push(shape);
});
}
// 为每个Shape创建填充和边框
shapes.forEach(shape => {
// 填充Mesh
const geometry = new THREE.ShapeGeometry(shape);
const material = new THREE.MeshBasicMaterial({
color: 0x3b82f6,
side: THREE.DoubleSide
});
const mesh = new THREE.Mesh(geometry, material);
// 绑定自定义数据(交互用)
mesh.userData = {
provinceName: provinceName,
originalColor: 0x3b82f6,
isHighlighted: false
};
scene.add(mesh);
provinceMeshes.push(mesh);
// 白色边框
const borderGeo = new THREE.BufferGeometry().setFromPoints(shape.getPoints());
const borderMat = new THREE.LineBasicMaterial({ color: 0xffffff, linewidth: 2 });
const border = new THREE.Line(borderGeo, borderMat);
mesh.border = border;
scene.add(border);
});
});
// 隐藏加载提示
document.querySelector('.loading').style.display = 'none';
} catch (err) {
console.error('地图加载失败:', err);
document.querySelector('.loading').textContent = '加载失败,请检查GeoJSON文件';
}
}
// ========== 4. 鼠标点击交互(Raycaster) ==========
/**
* 鼠标点击事件处理:省份高亮
* @param {MouseEvent} event - 鼠标事件
*/
function onDocumentMouseDown(event) {
// 1. 转换鼠标坐标为NDC(-1~1)
const mouse = new THREE.Vector2();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
// 2. 创建射线检测
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
// 3. 检测与省份Mesh的相交
const intersects = raycaster.intersectObjects(provinceMeshes);
if (intersects.length > 0) {
// 点击到省份:切换高亮
const clickedMesh = intersects[0].object;
// 取消之前的高亮
if (highlightedProvince) {
highlightedProvince.material.color.set(highlightedProvince.userData.originalColor);
highlightedProvince.border.material.color.set(0xffffff);
highlightedProvince.userData.isHighlighted = false;
}
// 高亮当前省份
clickedMesh.material.color.set(0xffd700); // 金色填充
clickedMesh.border.material.color.set(0xff0000); // 红色边框
clickedMesh.userData.isHighlighted = true;
highlightedProvince = clickedMesh;
console.log('点击省份:', clickedMesh.userData.provinceName);
} else {
// 点击空白处:取消所有高亮
if (highlightedProvince) {
highlightedProvince.material.color.set(highlightedProvince.userData.originalColor);
highlightedProvince.border.material.color.set(0xffffff);
highlightedProvince.userData.isHighlighted = false;
highlightedProvince = null;
}
}
}
window.addEventListener('click', onDocumentMouseDown, false);
// ========== 5. 窗口适配与渲染循环 ==========
// 窗口大小变化适配
window.addEventListener('resize', () => {
const aspect = window.innerWidth / window.innerHeight;
camera.left = -frustumSize * aspect / 2;
camera.right = frustumSize * aspect / 2;
camera.top = frustumSize / 2;
camera.bottom = -frustumSize / 2;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 渲染循环
function animate() {
requestAnimationFrame(animate);
controls.update(); // 更新控制器
renderer.render(scene, camera); // 渲染场景
}
animate();
// 启动地图加载
loadAndDrawMap();
</script>
</body>
</html>
总结与扩展建议
核心总结
-
2D地图核心:正交相机(
OrthographicCamera)是实现无透视2D效果的关键,区别于透视相机;
-
坐标转换:墨卡托投影是地理数据可视化的基础,需掌握经纬度→平面坐标的转换逻辑;
-
GeoJSON处理:递归遍历嵌套坐标、计算全局边界是地图居中缩放的核心;
-
交互实现:
Raycaster射线检测是Three.js实现鼠标点击交互的标准方式,userData是存储自定义数据的最佳实践;
-
性能优化:复用几何体/材质、减少不必要的顶点数,可提升大地图的渲染性能。
扩展建议
-
添加省份标签:基于省份中心坐标创建
CSS2DLabel,显示省份名称;
-
hover高亮:监听
mousemove事件,实现鼠标悬浮高亮;
-
数据可视化:根据省份数据(如GDP、人口)动态修改填充颜色;
-
层级优化:为南海诸岛等小区域单独缩放,提升显示效果;
-
性能优化:使用
BufferGeometry替代ShapeGeometry,减少内存占用;
-
地图交互增强:添加缩放限制(最小/最大缩放)、地图复位按钮。