由于最近在学习three.js,所以观摩了一下掘金,csdn等网站上的有关这部分的内容,刚好看到一个带你入门three.js——从0到1实现一个3d可视化地图 - 掘金 (juejin.cn),再加上我的专业属性是地理相关,可以说是专业对口,但文章已经是三年以前写的,而且没有在框架底下完成,有关three的很多API也发生了更改,所以我的思路是来自该篇文章,我进行了模仿和相应的修改,但是大致没有发生改变,可以说是站在前人的肩膀上。
首先就是开启一个vue项目,再npm install --save three,再引入一下d3就可以了,配置方面没有什么好配置的,这方面大家应该是没问题的。将代码写在子组件里,再引入到App.vue中展示就可以了。需要注意用到的全国的json数据来自DataV.GeoAtlas地理小工具系列 (aliyun.com)
子组件xx.vue对应代码
- <template>
- <div id="container" ref="canvasContainer">div>
- <div id="tooltip" ref="tooltip">div>
- template>
-
- <script setup>
- import * as THREE from 'three';
- //OrbitControls 是一个附加组件,必须显式导入
- import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
- //墨卡托投影转换可以把我们经纬度坐标转换成我们对应平面的2d坐标,d3里面自带墨卡托投影转换
- //该引入方式是查阅官网得到的
- import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
- import { onMounted, onUnmounted,ref } from 'vue';
-
- let canvasContainer = ref(null);
- let tooltip = ref(null)
- let scene,camera,renderer,ambientLight,raycaster,mouse;
- let lastPick = null;
-
- //初始化摄像机
- function initCamera(){
- camera = new THREE.PerspectiveCamera(75,canvasContainer.value.offsetWidth / canvasContainer.value.offsetHeight, 0.1, 1000);
- camera.position.set(0,0,120);
- camera.lookAt(scene.position);
- }
- //初始化renderer
- function initRenderer(){
- renderer = new THREE.WebGLRenderer();
- renderer.setSize(canvasContainer.value.offsetWidth,canvasContainer.value.offsetHeight)
- }
- //初始化灯光
- function initLight(){
- ambientLight = new THREE.AmbientLight(0xffffff,20);
- }
- //加载json数据
- function loadJson(){
- const loader = new THREE.FileLoader();
- loader.load('src/assets/中华人民共和国.json',(data)=>{
- const jsondata = JSON.parse(data);
- generateGeometry(jsondata)
- console.log(jsondata);
- })
- }
-
- // 根据JSON数据生成地图几何体
- function generateGeometry(jsondata){
- let map = new THREE.Object3D();
- // 使用d3的地图投影
- const projection = d3.geoMercator().center([104.0,37.5]).translate([0,0]);
- // 遍历每个省份,创建几何体
- jsondata.features.forEach((element)=>{
- let province = new THREE.Object3D();
- const coordinates = element.geometry.coordinates;
- if(Array.isArray(coordinates[0][0][0])){
- coordinates.forEach((multiPolygon)=>{
- multiPolygon.forEach((polygon)=>{
- const shape = new THREE.Shape();
- const points = [];
- polygon.forEach((coord,i)=>{
- const [x,y] = projection(coord);
- if(i===0) shape.moveTo(x,-y);
- else shape.lineTo(x,-y);
- points.push(new THREE.Vector3(x,-y,5));
- })
- const lineGeometry = new THREE.BufferGeometry().setFromPoints(points);
- const lineMaterial = new THREE.LineBasicMaterial({ color: 'white' });
- const line = new THREE.Line(lineGeometry, lineMaterial);
-
- const extrudeSettings = { depth: 10, bevelEnabled: false };
- const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
- const material = new THREE.MeshBasicMaterial({ color: '#2defff', transparent: true, opacity: 0.6 });
- const material1 = new THREE.MeshBasicMaterial({
- color: '#3480C4',
- transparent: true,
- opacity: 0.5,
- })
- const mesh = new THREE.Mesh(geometry, [material,material1]);
- province.properties = element.properties;
- province.add(mesh);
- province.add(line);
- })
- })
- }else if(Array.isArray(coordinates[0][0])){
- coordinates.forEach((polygon)=>{
- const shape = new THREE.Shape();
- const points = [];
- polygon.forEach((coord,i)=>{
- const [x,y] = projection(coord);
- if(i===0) shape.moveTo(x,-y);
- else shape.lineTo(x,-y);
- points.push(new THREE.Vector3(x,-y,5));
- })
- const lineGeometry = new THREE.BufferGeometry().setFromPoints(points);
- const lineMaterial = new THREE.LineBasicMaterial({ color: 'white' });
- const line = new THREE.Line(lineGeometry, lineMaterial);
-
- const extrudeSettings = { depth: 10, bevelEnabled: false };
- const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
- const material = new THREE.MeshBasicMaterial({ color: '#2defff', transparent: true, opacity: 0.6 });
- const material1 = new THREE.MeshBasicMaterial({
- color: '#3480C4',
- transparent: true,
- opacity: 0.5,
- })
- const mesh = new THREE.Mesh(geometry, [material,material1]);
- province.properties = element.properties;
- province.add(mesh);
- province.add(line);
- })
- }
- map.add(province);
- })
- scene.add(map);
- }
-
- // 设置光线投射器和鼠标位置,用于检测鼠标悬停对象
- function setRaycaster(){
- raycaster = new THREE.Raycaster();
- mouse = new THREE.Vector2();
- const onMouseMove = (event) => {
- mouse.x = (event.clientX / canvasContainer.value.offsetWidth) * 2 - 1
- mouse.y = -(event.clientY / canvasContainer.value.offsetHeight) * 2 + 1
- tooltip.value.style.left = event.clientX + 2 + 'px'
- tooltip.value.style.top = event.clientY + 2 + 'px'
- }
-
- window.addEventListener('mousemove', onMouseMove, false)
- }
-
- // 显示或隐藏工具提示
- function showTip(){
- if(lastPick){
- const properties = lastPick.object.parent.properties;
- tooltip.value.textContent = properties.name;
- tooltip.value.style.visibility = 'visible';
- console.log(tooltip.value.textContent);
- }else{
- tooltip.value.style.visibility = 'hidden';
- }
- }
-
- // 动画循环,用于渲染场景和更新状态
- function animate() {
- requestAnimationFrame(animate);
- raycaster.setFromCamera(mouse,camera);
- const intersects = raycaster.intersectObjects(scene.children,true);
- if (lastPick) {
- lastPick.object.material[0].color.set('#2defff')
- lastPick.object.material[1].color.set('#3480C4')
- }
- lastPick = null
- lastPick = intersects.find(
- (item) => item.object.material && item.object.material.length === 2
- )
- if (lastPick) {
- lastPick.object.material[0].color.set(0xff0000)
- lastPick.object.material[1].color.set(0xff0000)
- }
- showTip();
- renderer.render(scene, camera);
- }
-
- //窗口大小改变时,更新摄像机的宽高比和渲染器的大小
- function handleResize(){
- if(camera && renderer && canvasContainer.value){
- camera.aspect = canvasContainer.value.offsetWidth / canvasContainer.value.offsetHeight;
- camera.updateProjectionMatrix();
- renderer.setSize(canvasContainer.value.offsetWidth, canvasContainer.value.offsetHeight);
- }
- }
-
- // 组件挂载时的初始化逻辑
- onMounted(()=>{
- scene = new THREE.Scene();
- setRaycaster();
- initLight();
- scene.add(ambientLight);
- initCamera();
- loadJson();
- initRenderer();
- canvasContainer.value.appendChild(renderer.domElement);
- new OrbitControls(camera,canvasContainer.value)
- animate();
- window.addEventListener('resize',handleResize)
- })
-
- onUnmounted(()=>{
- window.removeEventListener('resize',handleResize)
- })
- script>
-
- <style>
- body{
- margin: 0;
- padding: 0;
- overflow: hidden;
- }
- #container{
- /* border: 1px solid black; */
- width: 100vw;
- height: 100vh;
- }
- #tooltip {
- position: absolute;
- z-index: 2;
- background: white;
- padding: 10px;
- border-radius: 5px;
- visibility: hidden;
- }
- style>
注意在用JSON数据生成地图集合体时分两种情况是因为:
不同省份数据数组嵌套的层数不一样,类似于下面这两地
共勉,如果对于实现的步骤还有疑惑,可以转至我在前言分享的那篇文章 ,它对于实现步骤更详细,可以结合着看。