今天我们来实现在Threejs中根据JSON格式数据创建三维地图的效果,先看下最终项目完成后的效果展示
通过读取JSON格式地图数据,获取地图边界及各区域边界经坐标,使用使用 D3.js 库中的 geoMercator() 方法进行墨卡托投影转换,将经纬度坐标转换为平面直角坐标系中的像素坐标,根据转换后的像素坐标,使用Threejs提供的ExtrudeGeometry方法对形状进行拉伸,形成三维立体效果;然后通过使用Threejs提供的Raycaster射线,获取鼠标与Mesh的焦点,并监听鼠标的mousemove移动事件和click点击事件,在mousemove移动事件处理Mesh变色,在click事件中对点击的边界进行拉伸,最终形成如图所示效果
JSON格式数据可以从阿里云提供的DataV工具中下载,具体下载地址为:https://datav.aliyun.com/portal/school/atlas/area_selector#&lat=30.332329214580188&lng=106.72278672066881&zoom=3.5,界面如下图所示:
打开页面后,选择你需要下载的省、市或者区县的边界地图,点击右面的下载即可,我这里选取的是杭州及其下属区县的地图JSON格式数据。
<template>
<MapView></MapView>
</template>
<script setup>
import MapView from './components/MapView.vue';
</script>
<style scoped>
</style>
style.css中的样式代码如下:
*{ margin: 0; padding: 0; list-style: none; }
<template>
<div id="scene"></div>
</template>
import * as THREE from 'three'
import * as d3 from 'd3'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { CSS2DRenderer,CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
import { onMounted } from 'vue';
import hangzhou from '../js/json/hangzhou.json'
let renderer, scene, camera, orbitControls, map, labelRenderer, mouse
let projectpos = d3
.geoMercator()
.center([120.21,30.25])
.scale(1600)
.translate([0,0])
function init() {
}
onMounted(() => {
init()
})
const initScene = () => {
scene = new THREE.Scene()
}
2 、initCamera方法 代码const initCamera = () => {
camera = new THREE.PerspectiveCamera(45,document.querySelector('#scene').clientWidth/document.querySelector('#scene').clientHeight)
camera.position.set(50,50,50)
camera.lookAt(scene.position)
}
3、initGeoJson方法代码const initGeoJson() => {
map = new THREE.Group()
initMap(hangzhou)
}
这里定义了一个initMap方法,该方法用于处理JSON数据,核心代码如下const initMap = (hangzhou) => {
hangzhou.features.forEach(element => {
// 定义一个province 的省份3D对象
const province = new THREE.Object3D()
// 结构hangzhou.json中features.geometry中的coordinates
// 对应的是每个点的坐标数组
let { coordinates } = element.geometry
initText(element.properties)
// 循环坐标数组
coordinates.forEach(multiPolygon => {
multiPolygon.forEach(polygon => {
// 定义shape对象
const shape = new THREE.Shape()
const lineMaterial = new THREE.LineBasicMaterial({
color: '#ffffff',
})
const lineGeometry = new THREE.BufferGeometry()
const pointsArray = new Array()
for (let i = 0; i < polygon.length; i++) {
let [x, y] = projectpos(polygon[i])
if (!isNaN(x)) {
shape.moveTo(x, -y)
}
shape.lineTo(x, -y)
pointsArray.push(new THREE.Vector3(x, -y, meshHeight))
}
lineGeometry.setFromPoints(pointsArray)
const extrudsSettings = {
depth: meshHeight,
bevelEnabled: false,// 对挤出的形状应用是否斜角
}
const geometry = new THREE.ExtrudeGeometry(shape, extrudsSettings)
const material = new THREE.MeshPhongMaterial({
color: '#4161ff',
transparent: true,
opacity: 0.7,
side: THREE.FrontSide
})
const material1 = new THREE.MeshLambertMaterial({
color: '#cfc5de',
transparent: true,
opacity: 0.7,
side: THREE.FrontSide
})
const mesh = new THREE.Mesh(geometry, [material, material1])
const line = new THREE.Line(lineGeometry, lineMaterial)
province.properties = element.properties
// 将城市信息方到模型中,后续做动画用
if (element.properties.centroid) {
const [x, y] = projectpos(element.properties.centroid)
province.properties._centroid = [x, y]
}
province.add(mesh)
province.add(line)
})
})
map.add(province)
})
scene.add(map)
}
4、initLight方法代码const initLight = () => {
const ambientLight = new THREE.AmbientLight(0x404040, 1.2)
scene.add(ambientLight)
// 平行光
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0)
scene.add(directionalLight)
// 点光源 - 照模型
const test = new THREE.PointLight("#ffffff", 1.8, 20)
test.position.set(1, -7, 7)
scene.add(test)
const testHelperMap = new THREE.PointLightHelper(test)
scene.add(testHelperMap)
}
5、setRaycaster方法代码const setRaycaster = () => {
rayCaster = new THREE.Raycaster()
mouse = new THREE.Vector2()
const onMouseMove = (event) => {
mouse.x = (event.clientX / document.querySelector('#scene').clientWidth) * 2 - 1
mouse.y = - (event.clientY / document.querySelector('#scene').clientHeight) * 2 + 1
}
// 点击地图事件
const onClick = (event) => {
if(selectedMesh === lastPick?.object){
return
}
// 恢复上一次清空的
if (lastPick && 'point' in lastPick) {
// 单击在Mesh上
const mesh = lastPick.object
if(selectedMesh) {
selectedMesh === mesh ? (resetMeshHeight(selectedMesh),selectedMesh = null) : (resetMeshHeight(selectedMesh),stretchMesh(mesh),selectedMesh = mesh)
} else {
// 没有选定的 Mesh,将选定的 Mesh 高度增加
// 伸展 mesh
stretchMesh(mesh)
selectedMesh = mesh
}
} else {
// 单击在 Mesh 区域外
if(selectedMesh) {
// 重置被选中的 mesh 的高度
resetMeshHeight(selectedMesh)
selectedMesh = null
}
// resetCameraTween()
}
}
window.addEventListener('mousemove', onMouseMove, false);
window.addEventListener('click', onClick, false);
}
6、initControls代码如下const initControls = () => {
orbitControls = new OrbitControls(camera, renderer.domElement)
orbitControls.minDistance = 2 //距离屏幕最近的距离
orbitControls.maxDistance = 5.5 //距屏幕最远距离
// 鼠标左右旋转幅度
orbitControls.minAzimuthAngle = -Math.PI / 4
orbitControls.maxAzimuthAngle = Math.PI / 4
// 鼠标上下转动幅度
// 鼠标上下转动幅度
orbitControls.minPolarAngle = Math.PI / 2
orbitControls.maxPolarAngle = Math.PI - 0.1
// 阻尼(惯性)
orbitControls.enableDamping = true
orbitControls.dampingFactor = 0.04
}
7、initRenderer代码如下const initRenderer = () => {
renderer = new THREE.WebGLRenderer({
antialias: true,
})
8、animate代码如下const animate = () => {
requestAnimationFrame(animate)
animationMouseover()
// city
animationCityWave()
animationCityEdgeLight()
animationCityMarker()
orbitControls.update()
renderer.render(scene, camera)
labelRenderer.render(scene, camera)
}
以上就是根据JSON格式数据创建三维地图的核心代码,所有代码完成后,刷新浏览器,可以看到效果如下: