基于react-hooks创建的三维地图,只实现了基本的交互展示,可根据个人喜好增加各种交互和展示效果,效果如下。
使用threejs创建3d地图注意的组要是以下几点。
完整代码如下,附注释。
import { useRef, useEffect, useCallback, useState } from 'react'
import * as THREE from 'three'
import * as D3 from 'd3'
import OrbitControls from 'three-orbitcontrols';
import './index.scss'
const View = () => {
const page = useRef(); // useRef不会导致重新渲染
/**
* 场景、相机、渲染器作为threejs的基本结构,需要在页面进入时渲染完毕
*/
const scence = useRef(new THREE.Scene()).current; //场景
const camera = useRef(new THREE.PerspectiveCamera()).current; //摄像机(透视投影)
const render = useRef(new THREE.WebGLRenderer()).current; //渲染器
const controls = new OrbitControls(camera, render.domElement);//创建控件对象
const timer = useRef(null) // 定义定时器
const handleProj = D3.geoMercator().center([109, 34.5]).scale(80).translate([0, 0]) // d3投影转换函数
const mapContainer = useRef(new THREE.Object3D()).current; // 存储地图Object3D对象
const lastPickedProvince = useRef(null) // 上次选中的省份
useEffect(() => {
page.current.appendChild(render.domElement);
initScene();
initLight();
initGeom();
renderScene();
}, [])
useEffect(() => {
bindEvent();
}, [])
const bindEvent = () => {
window.addEventListener('mousemove', (event) => {
const pointer = new THREE.Vector2();
// 像素坐标=>规范化设备坐标系 [-1,1]
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = - (event.clientY / window.innerHeight) * 2 + 1;
// 获取鼠标点击的位置生成射线
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(pointer, camera);
// 获取射线相交的物体集合
// debugger
const intersects = raycaster.intersectObjects(mapContainer.children, true);
if (intersects.length) {
const pcickedProvice = intersects[0].object;
// 选中了新的省份
if (lastPickedProvince.current?.properties !== pcickedProvice.properties) {
// 上次选中的恢复半透明
if (lastPickedProvince.current) {
lastPickedProvince.current.material.opacity = 0.5
}
pcickedProvice.material.opacity = 1; // 新选中的设置为不透明
lastPickedProvince.current = pcickedProvice;
}
} else { // 鼠标移开地图,之前选中的省份回复半透明
if (lastPickedProvince.current) {
lastPickedProvince.current.material.opacity = 0.5
}
lastPickedProvince.current = null;
}
}, false)
}
// 初始化场景
const initScene = useCallback(() => {
render.setSize(page.current.offsetWidth, page.current.offsetHeight); // 渲染器设置尺寸
// 设置背景颜色
render.setClearColor(new THREE.Color(0x000000)); // 设置背景颜色和透明度
render.shadowMap.enabled = true; // 渲染器允许渲染阴影⭐
/**
* 设置摄像机的属性
*/
camera.aspect = (page.current.offsetWidth / page.current.offsetHeight) // 摄像机设置屏幕宽高比
camera.fov = 45; // 摄像机的视角
camera.near = 0.01; // 近面距离
camera.far = 1001; // 远面距离
camera.position.set(2, 2, 200) // 设置摄像机在threejs坐标系中的位置
camera.lookAt(0, 0, 0) // 摄像机的指向
camera.updateProjectionMatrix(); // 更新摄像机投影矩阵,在任何参数被改变以后必须被调用
}, [render, scence])
// 初始化环境光
const initLight = () => {
const ambLight = new THREE.AmbientLight('#ffffff', 0.3) // 基本光源
/**
* 设置聚光灯相关的的属性,详情见P54
*/
const spotLight = new THREE.SpotLight(0xFFFFFF); // 聚光灯
spotLight.position.set(40, 200, 10);
spotLight.castShadow = true; // 只有该属性为true时,该点光源允许产生阴影,并且下列属性可用
scence.add(ambLight, spotLight); // 向场景中添加光源
}
// 初始化地理数据集
const initGeom = () => {
// 加载中国地区的geoJson数据集
const fileLoader = new THREE.FileLoader();
fileLoader.load(
'https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json',
(data) => {
const chinaJson = JSON.parse(data)
handleData(chinaJson)
}
)
}
// 处理GeoJson data
const handleData = (jsonData) => {
const feaureList = jsonData.features;
feaureList.forEach((feature) => { // 每个feature都代表一个省份
const province = new THREE.Object3D;
province.properties = feature.properties.name // 省份名称
province.name = feature.properties.name // 省份名称
const coordinates = feature.geometry.coordinates // 省份坐标信息
if (feature.geometry.type === 'MultiPolygon') {
coordinates.forEach((coord) => {
coord.forEach((coordinate) => {
const extrudeMesh = creatDepthPolygon(coordinate)
extrudeMesh.properties = feature.properties.name
province.add(extrudeMesh)
})
})
}
if (feature.geometry.type === 'Polygon') {
coordinates.forEach((coordinate) => {
const extrudeMesh = creatDepthPolygon(coordinate)
extrudeMesh.properties = feature.properties.name
province.add(extrudeMesh)
})
}
mapContainer.add(province)
})
scence.add(mapContainer)
}
// 创建三维多边形
const creatDepthPolygon = (coordinate) => {
const shape = new THREE.Shape();
coordinate.forEach((item, index) => { // 每一个item都是MultiPolygon中的一个polygon
const [x_XYZ, y_XYZ] = handleProj(item)
if (index === 0) {
shape.moveTo(x_XYZ, -y_XYZ)
} else {
shape.lineTo(x_XYZ, -y_XYZ)
}
})
const geometry = new THREE.ExtrudeBufferGeometry(shape, { depth: 1, bevelEnabled: false, })
const material = new THREE.MeshBasicMaterial({
color: new THREE.Color(Math.random() * 0xffffff), // 每个省随机赋色
transparent: true,
opacity: 0.5
})
return new THREE.Mesh(geometry, material)
}
// 渲染器执行渲染
const renderScene = useCallback(() => {
timer.current = window.requestAnimationFrame(() => renderScene())
controls.update();
render.render(scence, camera);
}, [render])
return (
<>
<div className='page' ref={page} >
<div style={{ position: 'fixed', top: '0', right: '0' }}>
<button onClick={() => { console.log(scence) }} style={{ marginRight: '10px' }}>打印场景</button>
</div>
</div>
</>
)
};
export default View