• Vue(3.3.4)+three.js(0.161.0)实现3D可视化地图


    一.前言


            由于最近在学习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对应代码

    1. <template>
    2. <div id="container" ref="canvasContainer">div>
    3. <div id="tooltip" ref="tooltip">div>
    4. template>
    5. <script setup>
    6. import * as THREE from 'three';
    7. //OrbitControls 是一个附加组件,必须显式导入
    8. import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
    9. //墨卡托投影转换可以把我们经纬度坐标转换成我们对应平面的2d坐标,d3里面自带墨卡托投影转换
    10. //该引入方式是查阅官网得到的
    11. import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
    12. import { onMounted, onUnmounted,ref } from 'vue';
    13. let canvasContainer = ref(null);
    14. let tooltip = ref(null)
    15. let scene,camera,renderer,ambientLight,raycaster,mouse;
    16. let lastPick = null;
    17. //初始化摄像机
    18. function initCamera(){
    19. camera = new THREE.PerspectiveCamera(75,canvasContainer.value.offsetWidth / canvasContainer.value.offsetHeight, 0.1, 1000);
    20. camera.position.set(0,0,120);
    21. camera.lookAt(scene.position);
    22. }
    23. //初始化renderer
    24. function initRenderer(){
    25. renderer = new THREE.WebGLRenderer();
    26. renderer.setSize(canvasContainer.value.offsetWidth,canvasContainer.value.offsetHeight)
    27. }
    28. //初始化灯光
    29. function initLight(){
    30. ambientLight = new THREE.AmbientLight(0xffffff,20);
    31. }
    32. //加载json数据
    33. function loadJson(){
    34. const loader = new THREE.FileLoader();
    35. loader.load('src/assets/中华人民共和国.json',(data)=>{
    36. const jsondata = JSON.parse(data);
    37. generateGeometry(jsondata)
    38. console.log(jsondata);
    39. })
    40. }
    41. // 根据JSON数据生成地图几何体
    42. function generateGeometry(jsondata){
    43. let map = new THREE.Object3D();
    44. // 使用d3的地图投影
    45. const projection = d3.geoMercator().center([104.0,37.5]).translate([0,0]);
    46. // 遍历每个省份,创建几何体
    47. jsondata.features.forEach((element)=>{
    48. let province = new THREE.Object3D();
    49. const coordinates = element.geometry.coordinates;
    50. if(Array.isArray(coordinates[0][0][0])){
    51. coordinates.forEach((multiPolygon)=>{
    52. multiPolygon.forEach((polygon)=>{
    53. const shape = new THREE.Shape();
    54. const points = [];
    55. polygon.forEach((coord,i)=>{
    56. const [x,y] = projection(coord);
    57. if(i===0) shape.moveTo(x,-y);
    58. else shape.lineTo(x,-y);
    59. points.push(new THREE.Vector3(x,-y,5));
    60. })
    61. const lineGeometry = new THREE.BufferGeometry().setFromPoints(points);
    62. const lineMaterial = new THREE.LineBasicMaterial({ color: 'white' });
    63. const line = new THREE.Line(lineGeometry, lineMaterial);
    64. const extrudeSettings = { depth: 10, bevelEnabled: false };
    65. const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
    66. const material = new THREE.MeshBasicMaterial({ color: '#2defff', transparent: true, opacity: 0.6 });
    67. const material1 = new THREE.MeshBasicMaterial({
    68. color: '#3480C4',
    69. transparent: true,
    70. opacity: 0.5,
    71. })
    72. const mesh = new THREE.Mesh(geometry, [material,material1]);
    73. province.properties = element.properties;
    74. province.add(mesh);
    75. province.add(line);
    76. })
    77. })
    78. }else if(Array.isArray(coordinates[0][0])){
    79. coordinates.forEach((polygon)=>{
    80. const shape = new THREE.Shape();
    81. const points = [];
    82. polygon.forEach((coord,i)=>{
    83. const [x,y] = projection(coord);
    84. if(i===0) shape.moveTo(x,-y);
    85. else shape.lineTo(x,-y);
    86. points.push(new THREE.Vector3(x,-y,5));
    87. })
    88. const lineGeometry = new THREE.BufferGeometry().setFromPoints(points);
    89. const lineMaterial = new THREE.LineBasicMaterial({ color: 'white' });
    90. const line = new THREE.Line(lineGeometry, lineMaterial);
    91. const extrudeSettings = { depth: 10, bevelEnabled: false };
    92. const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
    93. const material = new THREE.MeshBasicMaterial({ color: '#2defff', transparent: true, opacity: 0.6 });
    94. const material1 = new THREE.MeshBasicMaterial({
    95. color: '#3480C4',
    96. transparent: true,
    97. opacity: 0.5,
    98. })
    99. const mesh = new THREE.Mesh(geometry, [material,material1]);
    100. province.properties = element.properties;
    101. province.add(mesh);
    102. province.add(line);
    103. })
    104. }
    105. map.add(province);
    106. })
    107. scene.add(map);
    108. }
    109. // 设置光线投射器和鼠标位置,用于检测鼠标悬停对象
    110. function setRaycaster(){
    111. raycaster = new THREE.Raycaster();
    112. mouse = new THREE.Vector2();
    113. const onMouseMove = (event) => {
    114. mouse.x = (event.clientX / canvasContainer.value.offsetWidth) * 2 - 1
    115. mouse.y = -(event.clientY / canvasContainer.value.offsetHeight) * 2 + 1
    116. tooltip.value.style.left = event.clientX + 2 + 'px'
    117. tooltip.value.style.top = event.clientY + 2 + 'px'
    118. }
    119. window.addEventListener('mousemove', onMouseMove, false)
    120. }
    121. // 显示或隐藏工具提示
    122. function showTip(){
    123. if(lastPick){
    124. const properties = lastPick.object.parent.properties;
    125. tooltip.value.textContent = properties.name;
    126. tooltip.value.style.visibility = 'visible';
    127. console.log(tooltip.value.textContent);
    128. }else{
    129. tooltip.value.style.visibility = 'hidden';
    130. }
    131. }
    132. // 动画循环,用于渲染场景和更新状态
    133. function animate() {
    134. requestAnimationFrame(animate);
    135. raycaster.setFromCamera(mouse,camera);
    136. const intersects = raycaster.intersectObjects(scene.children,true);
    137. if (lastPick) {
    138. lastPick.object.material[0].color.set('#2defff')
    139. lastPick.object.material[1].color.set('#3480C4')
    140. }
    141. lastPick = null
    142. lastPick = intersects.find(
    143. (item) => item.object.material && item.object.material.length === 2
    144. )
    145. if (lastPick) {
    146. lastPick.object.material[0].color.set(0xff0000)
    147. lastPick.object.material[1].color.set(0xff0000)
    148. }
    149. showTip();
    150. renderer.render(scene, camera);
    151. }
    152. //窗口大小改变时,更新摄像机的宽高比和渲染器的大小
    153. function handleResize(){
    154. if(camera && renderer && canvasContainer.value){
    155. camera.aspect = canvasContainer.value.offsetWidth / canvasContainer.value.offsetHeight;
    156. camera.updateProjectionMatrix();
    157. renderer.setSize(canvasContainer.value.offsetWidth, canvasContainer.value.offsetHeight);
    158. }
    159. }
    160. // 组件挂载时的初始化逻辑
    161. onMounted(()=>{
    162. scene = new THREE.Scene();
    163. setRaycaster();
    164. initLight();
    165. scene.add(ambientLight);
    166. initCamera();
    167. loadJson();
    168. initRenderer();
    169. canvasContainer.value.appendChild(renderer.domElement);
    170. new OrbitControls(camera,canvasContainer.value)
    171. animate();
    172. window.addEventListener('resize',handleResize)
    173. })
    174. onUnmounted(()=>{
    175. window.removeEventListener('resize',handleResize)
    176. })
    177. script>
    178. <style>
    179. body{
    180. margin: 0;
    181. padding: 0;
    182. overflow: hidden;
    183. }
    184. #container{
    185. /* border: 1px solid black; */
    186. width: 100vw;
    187. height: 100vh;
    188. }
    189. #tooltip {
    190. position: absolute;
    191. z-index: 2;
    192. background: white;
    193. padding: 10px;
    194. border-radius: 5px;
    195. visibility: hidden;
    196. }
    197. style>

    注意在用JSON数据生成地图集合体时分两种情况是因为:

    不同省份数据数组嵌套的层数不一样,类似于下面这两地

     

    四.总结

            共勉,如果对于实现的步骤还有疑惑,可以转至我在前言分享的那篇文章 ,它对于实现步骤更详细,可以结合着看。

     

  • 相关阅读:
    加州驾照笔试准备笔记
    web课程设计网页规划与设计----公司官网带轮播图 (页面精美 1页)
    linux中的cd 切换目录、more 文件内容分屏查看器
    AWS 首席技术专家离职后加盟 MongoDB;Docker hub 发生中断;MLSQL 正式更名 Byzer,打造新一代开源语言生态 | 开源日报
    涨粉超100万,这些博主的内容密码是什么?
    pybind11使用总结(依赖python3.7版本)
    linux学习-用户组管理
    IAP固件升级进阶(Qt上位机)
    leetCode 852. 山脉数组的峰顶索引
    大数据(二)大数据架构发展史
  • 原文地址:https://blog.csdn.net/weixin_73810008/article/details/136403181