当前位置:   article > 正文

Three.js城市展示,可视化大屏_vue three.js 显示地图省份名字

vue three.js 显示地图省份名字

技术点

1、d3.js通过投影把地图数据的json映射到3维空间中,城市地图的json下载我就不多讲了,网上有很多教程,换成自己所需的城市就行;

2、地图上展示的数据展示的label,一开始用的sprite小精灵模型做的,但是会失真不清楚,后来换成了CSS2DRenderer这种方式,就相当于把html渲染到3维空间里,屡试不爽;

3、为了达到“酷炫智能”效果,在一加载和点击区县的时候,做了camera的动画(镜头移动、拉近),在这里就要在vue中引入tween.js了,tween做补间动画,还是很好用的;

4、地图边缘做了个流光效果,这个有很多厉害的博主介绍过,我是稍作了下修改;

5、每切换一个tab,隐藏/显示相应模型,所以把一组模型放到一组group里;

源码位置 

依赖版本 

 

 lien0219/vue2-test: vue2日常练习,后台管理简易模板 (github.com)

分解 

tween这个包提前去下载好,在main.js中声明

  1. // 补间动画
  2. import tween from "./utils/tween";
  3. Vue.use(ElementUI);
  4. Vue.use(tween);

 主要的代码Main.vue

  1. <template>
  2. <div>
  3. <div id="container"></div>
  4. <div id="tooltip"></div>
  5. <el-button-group class="button-group">
  6. <el-button type="" icon="" @click="groupOneChange">首页总览</el-button>
  7. <el-button type="" icon="" @click="groupTwoChange">应急管理</el-button>
  8. <el-button type="" icon="" @click="groupThreeChange">能源管理</el-button>
  9. <el-button type="" icon="" @click="groupFourChange">环境监测</el-button>
  10. <!-- <el-button type="" icon="">综合能源监控中心</el-button> -->
  11. </el-button-group>
  12. </div>
  13. </template>
  14. <script>
  15. import * as THREE from "three";
  16. import * as d3 from "d3";
  17. import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
  18. import {
  19. CSS2DRenderer,
  20. CSS2DObject,
  21. } from "three/examples/jsm/renderers/CSS2DRenderer.js";
  22. export default {
  23. data() {
  24. return {
  25. camera: null,
  26. scene: null,
  27. renderer: null,
  28. labelRenderer: null,
  29. container: null,
  30. // mesh: null,
  31. controller: null,
  32. map: null,
  33. raycaster: null,
  34. mouse: null,
  35. tooltip: null,
  36. lastPick: null,
  37. mapEdgeLightObj: {
  38. mapEdgePoints: [],
  39. lightOpacityGeometry: null, // 单独把geometry提出来,动画用
  40. // 边缘流光参数
  41. lightSpeed: 3,
  42. lightCurrentPos: 0,
  43. lightOpacitys: null,
  44. },
  45. // 每个屏幕模型一组
  46. groupOne: new THREE.Group(),
  47. groupTwo: new THREE.Group(),
  48. groupThree: new THREE.Group(),
  49. groupFour: new THREE.Group(),
  50. // groupOne 统计信息
  51. cityWaveMeshArr: [],
  52. cityCylinderMeshArr: [],
  53. cityMarkerMeshArr: [],
  54. cityNumMeshArr: [],
  55. // groupTwo 告警信息
  56. alarmWaveMeshArr: [],
  57. alarmCylinderMeshArr: [],
  58. alarmNameMeshArr: [],
  59. // groupThree 能源
  60. energyWaveMeshArr: [],
  61. energyCylinderMeshArr: [],
  62. energyNameMeshArr: [],
  63. // groupFour 环境
  64. monitorWaveMeshArr: [],
  65. monitorIconMeshArr: [],
  66. monitorNameMeshArr: [],
  67. // 城市信息
  68. mapConfig: {
  69. deep: 0.2,
  70. },
  71. // 摄像机移动位置,初始:0, -5, 1
  72. cameraPosArr: [
  73. // {x: 0.0, y: -0.3, z: 1},
  74. // {x: 5.0, y: 5.0, z: 2},
  75. // {x: 3.0, y: 3.0, z: 2},
  76. // {x: 0, y: 5.0, z: 2},
  77. // {x: -2.0, y: 3.0, z: 1},
  78. { x: 0, y: -3.0, z: 3.8 },
  79. ],
  80. // 数据 - 区县总数量
  81. dataTotal: [
  82. {
  83. name: "淄川区",
  84. adcode: "370302",
  85. total: 129,
  86. brief:
  87. "经营范围包括凭资质证从事炉窑工程专业承包贰级;工业窑炉热工设备、环保节能设备、机电设备、仪器仪表、电器的制造、销售及调试。",
  88. },
  89. {
  90. name: "张店区",
  91. adcode: "370303",
  92. total: 89,
  93. brief:
  94. "经营范围包括凭资质证从事炉窑工程专业承包贰级;工业窑炉热工设备、环保节能设备、机电设备、仪器仪表、电器的制造、销售及调试。",
  95. },
  96. {
  97. name: "博山区",
  98. adcode: "370304",
  99. total: 205,
  100. brief:
  101. "经营范围包括凭资质证从事炉窑工程专业承包贰级;工业窑炉热工设备、环保节能设备、机电设备、仪器仪表、电器的制造、销售及调试。",
  102. },
  103. {
  104. name: "沂源县",
  105. adcode: "370323",
  106. total: 26,
  107. brief:
  108. "经营范围包括凭资质证从事炉窑工程专业承包贰级;工业窑炉热工设备、环保节能设备、机电设备、仪器仪表、电器的制造、销售及调试。",
  109. },
  110. {
  111. name: "高青县",
  112. adcode: "370322",
  113. total: 8,
  114. brief:
  115. "经营范围包括凭资质证从事炉窑工程专业承包贰级;工业窑炉热工设备、环保节能设备、机电设备、仪器仪表、电器的制造、销售及调试。",
  116. },
  117. ],
  118. dataAlarm: [
  119. {
  120. name: "张店区",
  121. adcode: "370303",
  122. level: 1,
  123. type: "压力异常",
  124. content: "检测到压力过高,超过标准2000Pa",
  125. company: "窑炉5厂",
  126. location: "张店区万山村",
  127. tel: "18861899887",
  128. },
  129. {
  130. name: "沂源县",
  131. adcode: "370303",
  132. level: 1,
  133. type: "温度异常",
  134. content: "检测到温度2900­°C,超过标准1200-1800­°C",
  135. company: "窑炉1厂",
  136. location: "沂源县白塔镇南万山村",
  137. tel: "13561899812",
  138. },
  139. {
  140. name: "博山区",
  141. adcode: "370303",
  142. level: 2,
  143. type: "压力异常",
  144. content: "检测到压力过高,超过标准2000Pa",
  145. company: "窑炉2厂",
  146. location: "博山区白塔镇南万山村",
  147. tel: "14561899817",
  148. },
  149. {
  150. name: "临淄区",
  151. adcode: "370303",
  152. level: 3,
  153. type: "用水异常",
  154. content: "检测到用水异常,超过标准10万吨",
  155. company: "窑炉3厂",
  156. location: "临淄区南万山村",
  157. tel: "18061899829",
  158. },
  159. ],
  160. dataEnergy: [
  161. {
  162. name: "张店区",
  163. adcode: "370303",
  164. level: 1,
  165. type: "用水异常",
  166. content: "检测到用水异常",
  167. company: "窑炉5厂",
  168. location: "张店区万山村",
  169. tel: "18861899887",
  170. },
  171. {
  172. name: "高青县",
  173. adcode: "370303",
  174. level: 1,
  175. type: "用电异常",
  176. content: "检测到用电异常",
  177. company: "窑炉1厂",
  178. location: "沂源县白塔镇南万山村",
  179. tel: "13561899812",
  180. },
  181. {
  182. name: "淄川区",
  183. adcode: "370303",
  184. level: 2,
  185. type: "用气异常",
  186. content: "检测到用气异常",
  187. company: "窑炉2厂",
  188. location: "博山区白塔镇南万山村",
  189. tel: "14561899817",
  190. },
  191. ],
  192. dataMonitor: [
  193. {
  194. name: "临淄区",
  195. adcode: "370303",
  196. monitor: "监控点一",
  197. // type: 2,
  198. content: "正常",
  199. company: "窑炉5厂",
  200. location: "张店区万山村",
  201. },
  202. {
  203. name: "张店区",
  204. adcode: "370303",
  205. monitor: "监控点二",
  206. // type: 1,
  207. content: "正常",
  208. company: "窑炉1厂",
  209. location: "沂源县白塔镇南万山村",
  210. },
  211. {
  212. name: "淄川区",
  213. adcode: "370303",
  214. monitor: "监控点三",
  215. // type: 2,
  216. content: "正常",
  217. company: "窑炉2厂",
  218. location: "博山区白塔镇南万山村",
  219. },
  220. ],
  221. };
  222. },
  223. mounted() {
  224. this.init();
  225. this.animate();
  226. window.addEventListener("resize", this.onWindowSize);
  227. },
  228. methods: {
  229. //初始化
  230. init() {
  231. this.container = document.getElementById("container");
  232. this.setScene();
  233. this.setCamera();
  234. this.setRenderer(); // 创建渲染器对象
  235. this.setController(); // 创建控件对象
  236. this.addHelper();
  237. this.loadMapData();
  238. this.setEarth();
  239. this.setRaycaster();
  240. this.setLight();
  241. },
  242. setScene() {
  243. // 创建场景对象Scene
  244. this.scene = new THREE.Scene();
  245. },
  246. setCamera() {
  247. // 第二参数就是 长度和宽度比 默认采用浏览器 返回以像素为单位的窗口的内部宽度和高度
  248. this.camera = new THREE.PerspectiveCamera(
  249. 75,
  250. window.innerWidth / window.innerHeight,
  251. 0.1,
  252. 500
  253. );
  254. this.camera.position.set(0, -5, 1); // 0, -5, 1
  255. this.camera.lookAt(new THREE.Vector3(0, 0, 0)); // 0, 0, 0 this.scene.position
  256. },
  257. setRenderer() {
  258. this.renderer = new THREE.WebGLRenderer({
  259. antialias: true,
  260. // logarithmicDepthBuffer: true, // 是否使用对数深度缓存
  261. });
  262. this.renderer.setSize(
  263. this.container.clientWidth,
  264. this.container.clientHeight
  265. );
  266. this.renderer.setPixelRatio(window.devicePixelRatio);
  267. // this.renderer.sortObjects = false; // 是否需要对对象排序
  268. this.container.appendChild(this.renderer.domElement);
  269. this.labelRenderer = new CSS2DRenderer();
  270. this.labelRenderer.setSize(
  271. this.container.clientWidth,
  272. this.container.clientHeight
  273. );
  274. this.labelRenderer.domElement.style.position = "absolute";
  275. this.labelRenderer.domElement.style.top = 0;
  276. this.container.appendChild(this.labelRenderer.domElement);
  277. },
  278. setController() {
  279. this.controller = new OrbitControls(
  280. this.camera,
  281. this.labelRenderer.domElement
  282. );
  283. this.controller.minDistance = 2;
  284. this.controller.maxDistance = 5.5; // 5.5
  285. // 阻尼(惯性)
  286. // this.controller.enableDamping = true;
  287. // this.controller.dampingFactor = 0.04;
  288. this.controller.minAzimuthAngle = -Math.PI / 4;
  289. this.controller.maxAzimuthAngle = Math.PI / 4;
  290. this.controller.minPolarAngle = 1;
  291. this.controller.maxPolarAngle = Math.PI - 0.1;
  292. // 修改相机的lookAt是不会影响THREE.OrbitControls的target的
  293. // this.controller.target = new THREE.Vector3(0, -5, 2);
  294. },
  295. // 辅助线
  296. addHelper() {
  297. // let helper = new THREE.CameraHelper(this.camera);
  298. // this.scene.add(helper);
  299. //轴辅助 (每一个轴的长度)
  300. let axisHelper = new THREE.AxisHelper(150); // 红线是X轴,绿线是Y轴,蓝线是Z轴
  301. // this.scene.add(axisHelper);
  302. let gridHelper = new THREE.GridHelper(100, 30, 0x2c2c2c, 0x888888);
  303. // this.scene.add(gridHelper);
  304. },
  305. setLight() {
  306. const ambientLight = new THREE.AmbientLight(0x404040, 1.2);
  307. this.scene.add(ambientLight);
  308. // // 平行光
  309. const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
  310. this.scene.add(directionalLight);
  311. // 聚光光源 - 照模型
  312. // const spotLight = new THREE.SpotLight(0xffffff, 0.9);
  313. // spotLight.position.set(1, -4, 4);
  314. // spotLight.castShadow = true;
  315. // this.scene.add(spotLight);
  316. // 聚光光源辅助线
  317. // const spotLightHelper = new THREE.SpotLightHelper(spotLight);
  318. // this.scene.add(spotLightHelper);
  319. // 点光源 - 照模型
  320. const test = new THREE.PointLight("#ffffff", 1.8, 20);
  321. test.position.set(1, -7, 7);
  322. this.scene.add(test);
  323. const testHelperMap = new THREE.PointLightHelper(test);
  324. this.scene.add(testHelperMap);
  325. // 点光源 - 蓝色照地球
  326. const pointLightMap = new THREE.PointLight("#4161ff", 1.4, 20);
  327. pointLightMap.position.set(0, 7, 3);
  328. this.scene.add(pointLightMap);
  329. const spotLightHelperMap = new THREE.PointLightHelper(pointLightMap);
  330. // this.scene.add(spotLightHelperMap);
  331. },
  332. // 加载地图数据
  333. loadMapData() {
  334. const loader = new THREE.FileLoader();
  335. loader.load("/static/map/json/zibo.json", (data) => {
  336. const jsondata = JSON.parse(data);
  337. this.addMapGeometry(jsondata);
  338. });
  339. },
  340. // 地图模型
  341. addMapGeometry(jsondata) {
  342. // 初始化一个地图对象
  343. this.map = new THREE.Object3D();
  344. // 墨卡托投影转换
  345. const projection = d3
  346. .geoMercator()
  347. .center([118.2, 36.7]) // 淄博市
  348. // .scale(2000)
  349. .translate([0.2, 0.15]); // 根据地球贴图做轻微调整
  350. jsondata.features.forEach((elem) => {
  351. // 定一个省份3D对象
  352. const province = new THREE.Object3D();
  353. // 每个的 坐标 数组
  354. const coordinates = elem.geometry.coordinates;
  355. // 循环坐标数组
  356. coordinates.forEach((multiPolygon) => {
  357. multiPolygon.forEach((polygon) => {
  358. const shape = new THREE.Shape();
  359. const lineMaterial = new THREE.LineBasicMaterial({
  360. color: "#ffffff",
  361. // linewidth: 1,
  362. // linecap: 'round', //ignored by WebGLRenderer
  363. // linejoin: 'round' //ignored by WebGLRenderer
  364. });
  365. // const lineGeometry = new THREE.Geometry();
  366. // for (let i = 0; i < polygon.length; i++) {
  367. // const [x, y] = projection(polygon[i]);
  368. // if (i === 0) {
  369. // shape.moveTo(x, -y);
  370. // }
  371. // shape.lineTo(x, -y);
  372. // lineGeometry.vertices.push(new THREE.Vector3(x, -y, 3));
  373. // }
  374. const lineGeometry = new THREE.BufferGeometry();
  375. const pointsArray = new Array();
  376. for (let i = 0; i < polygon.length; i++) {
  377. const [x, y] = projection(polygon[i]);
  378. if (i === 0) {
  379. shape.moveTo(x, -y);
  380. }
  381. shape.lineTo(x, -y);
  382. pointsArray.push(new THREE.Vector3(x, -y, this.mapConfig.deep));
  383. // 做边缘流光效果,把所有点保存下来
  384. this.mapEdgeLightObj.mapEdgePoints.push([
  385. x,
  386. -y,
  387. this.mapConfig.deep,
  388. ]);
  389. }
  390. // console.log(pointsArray);
  391. lineGeometry.setFromPoints(pointsArray);
  392. const extrudeSettings = {
  393. depth: this.mapConfig.deep,
  394. bevelEnabled: false, // 对挤出的形状应用是否斜角
  395. };
  396. const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
  397. const material = new THREE.MeshPhongMaterial({
  398. color: "#4161ff",
  399. transparent: true,
  400. opacity: 0.4,
  401. side: THREE.FrontSide,
  402. // depthTest: true,
  403. });
  404. const material1 = new THREE.MeshLambertMaterial({
  405. color: "#10004a",
  406. transparent: true,
  407. opacity: 0.7,
  408. side: THREE.FrontSide,
  409. // wireframe: true
  410. });
  411. const mesh = new THREE.Mesh(geometry, [material, material1]);
  412. const line = new THREE.Line(lineGeometry, lineMaterial);
  413. // 将省份的属性 加进来
  414. province.properties = elem.properties;
  415. // 将城市信息放到模型中,后续做动画用
  416. if (elem.properties.centroid) {
  417. const [x, y] = projection(elem.properties.centroid); // uv映射坐标
  418. province.properties._centroid = [x, y];
  419. }
  420. // console.log(elem.properties);
  421. province.add(mesh);
  422. province.add(line);
  423. });
  424. });
  425. // province.scale.set(5, 5, 0);
  426. // province.position.set(0, 0, 0);
  427. // console.log(province);
  428. this.map.add(province);
  429. });
  430. this.setMapEdgeLight();
  431. this.setMapName();
  432. this.scene.add(this.map);
  433. // 获取数据后,加载模型
  434. this.getResponseData();
  435. },
  436. // 地图边缘流光效果
  437. setMapEdgeLight() {
  438. // console.log(this.mapEdgeLightObj.mapEdgePoints);
  439. let positions = new Float32Array(
  440. this.mapEdgeLightObj.mapEdgePoints.flat(1)
  441. ); // 数组深度遍历扁平化
  442. // console.log(positions);
  443. this.mapEdgeLightObj.lightOpacityGeometry = new THREE.BufferGeometry();
  444. // 设置顶点
  445. this.mapEdgeLightObj.lightOpacityGeometry.setAttribute(
  446. "position",
  447. new THREE.BufferAttribute(positions, 3)
  448. );
  449. // 设置 粒子透明度为 0
  450. this.mapEdgeLightObj.lightOpacitys = new Float32Array(
  451. positions.length
  452. ).map(() => 0);
  453. this.mapEdgeLightObj.lightOpacityGeometry.setAttribute(
  454. "aOpacity",
  455. new THREE.BufferAttribute(this.mapEdgeLightObj.lightOpacitys, 1)
  456. );
  457. // 顶点着色器
  458. const vertexShader = `
  459. attribute float aOpacity;
  460. uniform float uSize;
  461. varying float vOpacity;
  462. void main(){
  463. gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.0);
  464. gl_PointSize = uSize;
  465. vOpacity=aOpacity;
  466. }
  467. `;
  468. // 片段着色器
  469. const fragmentShader = `
  470. varying float vOpacity;
  471. uniform vec3 uColor;
  472. float invert(float n){
  473. return 1.-n;
  474. }
  475. void main(){
  476. if(vOpacity <=0.2){
  477. discard;
  478. }
  479. vec2 uv=vec2(gl_PointCoord.x,invert(gl_PointCoord.y));
  480. vec2 cUv=2.*uv-1.;
  481. vec4 color=vec4(1./length(cUv));
  482. color*=vOpacity;
  483. color.rgb*=uColor;
  484. gl_FragColor=color;
  485. }
  486. `;
  487. const material = new THREE.ShaderMaterial({
  488. vertexShader: vertexShader,
  489. fragmentShader: fragmentShader,
  490. transparent: true, // 设置透明
  491. // blending: THREE.AdditiveBlending,
  492. uniforms: {
  493. uSize: {
  494. value: 5.0,
  495. },
  496. uColor: {
  497. value: new THREE.Color("#ffffff"), // 光点颜色 fffb85
  498. },
  499. },
  500. });
  501. // material.blending = THREE.AdditiveBlending;
  502. const opacityPointsMesh = new THREE.Points(
  503. this.mapEdgeLightObj.lightOpacityGeometry,
  504. material
  505. );
  506. this.scene.add(opacityPointsMesh);
  507. },
  508. // 地球贴图纹理
  509. setEarth() {
  510. const geometry = new THREE.PlaneGeometry(14.0, 14.0);
  511. const texture = new THREE.TextureLoader().load(
  512. "/static/map/texture/earth.jpg"
  513. );
  514. const bumpTexture = new THREE.TextureLoader().load(
  515. "/static/map/texture/earth.jpg"
  516. );
  517. // texture.wrapS = THREE.RepeatWrapping; // 质地.包裹
  518. // texture.wrapT = THREE.RepeatWrapping;
  519. const material = new THREE.MeshPhongMaterial({
  520. map: texture, // 贴图
  521. bumpMap: bumpTexture,
  522. bumpScale: 0.05,
  523. // specularMap: texture,
  524. // specular: 0xffffff,
  525. // shininess: 1,
  526. // color: "#000000",
  527. side: THREE.FrontSide,
  528. });
  529. const earthPlane = new THREE.Mesh(geometry, material);
  530. this.scene.add(earthPlane);
  531. },
  532. // 地图label
  533. setMapName() {
  534. this.map.children.forEach((elem, index) => {
  535. // 找到中心点
  536. const y = -elem.properties._centroid[1];
  537. const x = elem.properties._centroid[0];
  538. // 转化为二维坐标
  539. const vector = new THREE.Vector3(x, y, this.mapConfig.deep + 0.01);
  540. // 添加城市名称
  541. this.setCityName(vector, elem.properties.name);
  542. });
  543. },
  544. // 获取数据后,加载模型
  545. getResponseData() {
  546. let self = this;
  547. setTimeout(function () {
  548. self.addCityModel();
  549. self.addAlarmModel();
  550. self.addEnergyModel();
  551. self.addMonitorModel();
  552. // 初始化动画
  553. setTimeout(self.cameraTween, 1000);
  554. }, 500);
  555. },
  556. // 地区中心点 - 获取向量
  557. mapElem2Centroid(elem) {
  558. // 找到中心点
  559. const y = -elem.properties._centroid[1];
  560. const x = elem.properties._centroid[0];
  561. // 转化为二维坐标
  562. const vector = new THREE.Vector3(x, y, this.mapConfig.deep + 0.01);
  563. return vector;
  564. },
  565. // 数据归一化,映射到0-1区间 - 获取最大值
  566. getMaxV(distributionInfo) {
  567. let max = 0;
  568. for (let item of distributionInfo) {
  569. if (max < item.total) max = item.total;
  570. }
  571. return max;
  572. },
  573. // 数据归一化,映射到0-1区间 - 获取最小值
  574. getMinV(distributionInfo) {
  575. let min = 1000000;
  576. for (let item of distributionInfo) {
  577. if (min > item.total) min = item.total;
  578. }
  579. return min;
  580. },
  581. // 数据归一化,映射到0-1区间
  582. normalization(data, min, max) {
  583. let normalizationRatio = (data - min) / (max - min);
  584. return normalizationRatio;
  585. },
  586. // GroupOne 添加模型
  587. addCityModel() {
  588. // 数据归一化
  589. const min = this.getMinV(this.dataTotal);
  590. const max = this.getMaxV(this.dataTotal);
  591. // 添加模型
  592. this.map.children.forEach((elem, index) => {
  593. // console.log(elem);
  594. // 满足数据条件 dataTotal
  595. if (this.dataTotal) {
  596. const vector = this.mapElem2Centroid(elem);
  597. this.dataTotal.forEach((d) => {
  598. // 数据归一化,映射到0-1区间
  599. let num = this.normalization(d.total, min, max);
  600. // 判断区县
  601. if (d.name === elem.properties.name) {
  602. // 添加城市光波
  603. this.setCityWave(vector);
  604. // 添加城市标记
  605. this.setCityMarker(vector);
  606. // 添加城市光柱
  607. this.setCityCylinder(vector, num);
  608. // 添加城市数据
  609. this.setCityNum(vector, num, d);
  610. }
  611. });
  612. this.scene.add(this.groupOne);
  613. }
  614. });
  615. },
  616. // GroupTwo 添加模型
  617. addAlarmModel() {
  618. this.map.children.forEach((elem, index) => {
  619. // console.log(elem);
  620. // 满足数据条件 dataAlarm
  621. if (this.dataAlarm) {
  622. const vector = this.mapElem2Centroid(elem);
  623. // 各等级颜色 123
  624. const colorLevel = ["#ff1800", "#FF8A00", "#FAE52D"];
  625. this.dataAlarm.forEach((d) => {
  626. // 判断区县
  627. if (d.name === elem.properties.name) {
  628. // 添加告警光波
  629. this.setAlarmWave(vector, colorLevel[d.level - 1]);
  630. // 添加告警标记
  631. this.setAlarmCylinder(vector, colorLevel[d.level - 1]);
  632. // 添加告警名称
  633. this.setAlarmName(vector, colorLevel[d.level - 1], d);
  634. }
  635. });
  636. // 先隐藏,通过按钮控制
  637. this.groupTwo.visible = false;
  638. this.scene.add(this.groupTwo);
  639. }
  640. });
  641. },
  642. // GroupThree 添加模型
  643. addEnergyModel() {
  644. this.map.children.forEach((elem, index) => {
  645. // console.log(elem);
  646. // 满足数据条件 dataEnergy
  647. if (this.dataEnergy) {
  648. const vector = this.mapElem2Centroid(elem);
  649. // 各等级颜色 123
  650. const colorLevel = ["#ff1800", "#FF8A00", "#FAE52D"];
  651. this.dataEnergy.forEach((d) => {
  652. // 判断区县
  653. if (d.name === elem.properties.name) {
  654. // 添加能源光波
  655. this.setEnergyWave(vector, colorLevel[d.level - 1]);
  656. // 添加能源标记
  657. this.setEnergyCylinder(vector, colorLevel[d.level - 1]);
  658. // 添加能源名称
  659. this.setEnergyName(vector, colorLevel[d.level - 1], d);
  660. }
  661. });
  662. // 先隐藏,通过按钮控制
  663. this.groupThree.visible = false;
  664. this.scene.add(this.groupThree);
  665. }
  666. });
  667. },
  668. // GroupFour 添加模型
  669. addMonitorModel() {
  670. this.map.children.forEach((elem, index) => {
  671. // console.log(elem);
  672. // 满足数据条件 dataMonitor
  673. if (this.dataMonitor) {
  674. const vector = this.mapElem2Centroid(elem);
  675. // 各等级颜色 123
  676. this.dataMonitor.forEach((d) => {
  677. // 判断区县
  678. if (d.name === elem.properties.name) {
  679. // 添加监测光波
  680. this.setMonitorWave(vector);
  681. // 添加监测标记
  682. this.setMonitorIcon(vector);
  683. // 添加监测名称
  684. this.setMonitorName(vector, d);
  685. }
  686. });
  687. // 先隐藏,通过按钮控制
  688. this.groupFour.visible = false;
  689. this.scene.add(this.groupFour);
  690. }
  691. });
  692. },
  693. // 城市 - 光柱
  694. setCityCylinder(vector, num) {
  695. const height = num;
  696. const geometry = new THREE.CylinderGeometry(0.08, 0.08, height, 20);
  697. // 顶点着色器
  698. const vertexShader = `
  699. uniform vec3 viewVector;
  700. varying float intensity;
  701. void main() {
  702. gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4( position, 1.0 );
  703. vec3 actual_normal = vec3(modelMatrix * vec4(normal, 0.0));
  704. intensity = pow(dot(normalize(viewVector), actual_normal), 3.0);
  705. }
  706. `;
  707. // 片段着色器
  708. const fragmentShader = `
  709. varying float intensity;
  710. void main() {
  711. vec3 glow = vec3(246, 239, 0) * 3.0;
  712. gl_FragColor = vec4(glow, 1);
  713. }
  714. `;
  715. let material = new THREE.MeshPhongMaterial({
  716. // ShaderMaterial
  717. // uniforms: {
  718. // viewVector: this.camera.position
  719. // },
  720. // vertexShader: vertexShader,
  721. // fragmentShader: fragmentShader,
  722. color: "#ede619",
  723. side: THREE.FrontSide,
  724. blending: THREE.AdditiveBlending,
  725. transparent: true,
  726. // depthTest: false,
  727. precision: "mediump",
  728. // depthFunc: THREE.LessEqualDepth,
  729. opacity: 0.9,
  730. });
  731. const cylinder = new THREE.Mesh(geometry, material);
  732. cylinder.position.set(vector.x, vector.y, vector.z + height / 2);
  733. cylinder.rotateX(Math.PI / 2);
  734. cylinder.scale.set(1, 1, 1);
  735. // cylinder.position.z -= height / 2;
  736. // cylinder.translateY(-height);
  737. cylinder._height = height;
  738. // 法向量计算位置
  739. // let coordVec3 = vector.normalize();
  740. // // mesh默认在XOY平面上,法线方向沿着z轴new THREE.Vector3(0, 0, 1)
  741. // let meshNormal = new THREE.Vector3(0, 0, 0);
  742. // // 四元数属性,角度旋转,quaternion表示mesh的角度状态,setFromUnitVectors();计算两个向量之间构成的四元数值
  743. // cylinder.quaternion.setFromUnitVectors(meshNormal, coordVec3);
  744. this.cityCylinderMeshArr.push(cylinder);
  745. this.groupOne.add(cylinder);
  746. // this.scene.add(cylinder);
  747. },
  748. // 城市 - 光波
  749. setCityWave(vector) {
  750. const cityGeometry = new THREE.PlaneBufferGeometry(1, 1); //默认在XOY平面上
  751. const textureLoader = new THREE.TextureLoader(); // TextureLoader创建一个纹理加载器对象
  752. const texture = textureLoader.load("/static/map/texture/wave.png");
  753. // 如果不同mesh材质的透明度、颜色等属性同一时刻不同,材质不能共享
  754. const cityWaveMaterial = new THREE.MeshBasicMaterial({
  755. color: "#ede619", // 0x22ffcc
  756. map: texture,
  757. transparent: true, //使用背景透明的png贴图,注意开启透明计算
  758. opacity: 1.0,
  759. side: THREE.FrontSide, //双面可见
  760. depthWrite: false, //禁止写入深度缓冲区数据
  761. blending: THREE.AdditiveBlending,
  762. });
  763. let cityWaveMesh = new THREE.Mesh(cityGeometry, cityWaveMaterial);
  764. cityWaveMesh.position.set(vector.x, vector.y, vector.z);
  765. cityWaveMesh.size = 0;
  766. // cityWaveMesh.scale.set(0.1, 0.1, 0.1); // 设置mesh大小
  767. // 法向量计算位置
  768. // let coordVec3 = vector.normalize();
  769. // // mesh默认在XOY平面上,法线方向沿着z轴new THREE.Vector3(0, 0, 1)
  770. // let meshNormal = new THREE.Vector3(0, 0, 0);
  771. // // 四元数属性,角度旋转,quaternion表示mesh的角度状态,setFromUnitVectors();计算两个向量之间构成的四元数值
  772. // cityWaveMesh.quaternion.setFromUnitVectors(meshNormal, coordVec3);
  773. this.cityWaveMeshArr.push(cityWaveMesh);
  774. this.groupOne.add(cityWaveMesh);
  775. // 添加到场景中
  776. // this.scene.add(cityWaveMesh);
  777. },
  778. // 城市 - 标记
  779. setCityMarker(vector) {
  780. const cityGeometry = new THREE.PlaneBufferGeometry(0.3, 0.3); //默认在XOY平面上
  781. const textureLoader = new THREE.TextureLoader(); // TextureLoader创建一个纹理加载器对象
  782. const texture = textureLoader.load("/static/map/texture/marker.png");
  783. // 如果不同mesh材质的透明度、颜色等属性同一时刻不同,材质不能共享
  784. const cityMaterial = new THREE.MeshBasicMaterial({
  785. color: "#ffe000", // 0x22ffcc
  786. map: texture,
  787. transparent: true, //使用背景透明的png贴图,注意开启透明计算
  788. opacity: 1.0,
  789. side: THREE.FrontSide, //双面可见
  790. depthWrite: false, //禁止写入深度缓冲区数据
  791. blending: THREE.AdditiveBlending,
  792. });
  793. cityMaterial.blending = THREE.CustomBlending;
  794. cityMaterial.blendSrc = THREE.SrcAlphaFactor;
  795. cityMaterial.blendDst = THREE.DstAlphaFactor;
  796. cityMaterial.blendEquation = THREE.AddEquation;
  797. let cityMarkerMesh = new THREE.Mesh(cityGeometry, cityMaterial);
  798. cityMarkerMesh.position.set(vector.x, vector.y, vector.z);
  799. cityMarkerMesh.size = 0;
  800. // cityWaveMesh.scale.set(0.1, 0.1, 0.1); // 设置mesh大小
  801. this.cityMarkerMeshArr.push(cityMarkerMesh);
  802. this.groupOne.add(cityMarkerMesh);
  803. // 添加到场景中
  804. // this.scene.add(cityMarkerMesh);
  805. },
  806. // 城市 - 数据显示
  807. setCityNum(vector, num, data) {
  808. // CSS2DRenderer生成的标签直接就是挂在真实的DOM上,并非是Vue的虚拟DOM上
  809. const div = document.createElement("div");
  810. div.className = "city-num-label";
  811. div.textContent = data.total;
  812. const contentDiv = document.createElement("div");
  813. contentDiv.className = "city-num-label-content";
  814. contentDiv.innerHTML =
  815. "本区县共有窑炉企业 " +
  816. data.total +
  817. " 个。<br/>" +
  818. "介绍:" +
  819. data.brief;
  820. div.appendChild(contentDiv);
  821. const label = new CSS2DObject(div);
  822. label.position.set(vector.x, vector.y, num + 0.5);
  823. label.visible = true;
  824. this.cityNumMeshArr.push(label);
  825. this.groupOne.add(label);
  826. // this.scene.add(spritey);
  827. },
  828. // 城市 - 名称显示
  829. setCityName(vector, name) {
  830. let spritey = this.makeTextSprite(name, {
  831. fontface: "微软雅黑",
  832. fontsize: 28, //100调整位置,下面通过scale缩放
  833. fontColor: { r: 255, g: 255, b: 255, a: 1.0 },
  834. borderColor: { r: 94, g: 94, b: 94, a: 0.0 },
  835. backgroundColor: { r: 255, g: 255, b: 0, a: 0.0 },
  836. borderThickness: 2,
  837. round: 6,
  838. });
  839. // 轻微偏移,错开光柱
  840. spritey.position.set(vector.x + 0.06, vector.y + 0.0, 0.22); // num + 0.3
  841. this.scene.add(spritey);
  842. },
  843. // 城市 - 名称显示 - 小精灵mesh
  844. makeTextSprite(message, parameters) {
  845. if (parameters === undefined) parameters = {};
  846. let fontface = parameters["fontface"];
  847. let fontsize = parameters["fontsize"];
  848. let fontColor = parameters["fontColor"];
  849. let borderThickness = parameters["borderThickness"];
  850. let borderColor = parameters["borderColor"];
  851. let backgroundColor = parameters["backgroundColor"];
  852. // var spriteAlignment = THREE.SpriteAlignment.topLeft;
  853. let canvas = document.createElement("canvas");
  854. let context = canvas.getContext("2d");
  855. context.font = "Bold " + fontsize + "px " + fontface;
  856. // get size data (height depends only on font size)
  857. let metrics = context.measureText(message);
  858. let textWidth = metrics.width;
  859. // background color
  860. context.fillStyle =
  861. "rgba(" +
  862. backgroundColor.r +
  863. "," +
  864. backgroundColor.g +
  865. "," +
  866. backgroundColor.b +
  867. "," +
  868. backgroundColor.a +
  869. ")";
  870. // border color
  871. context.strokeStyle =
  872. "rgba(" +
  873. borderColor.r +
  874. "," +
  875. borderColor.g +
  876. "," +
  877. borderColor.b +
  878. "," +
  879. borderColor.a +
  880. ")";
  881. context.lineWidth = borderThickness;
  882. const painting = {
  883. width: textWidth * 1.4 + borderThickness * 2,
  884. height: fontsize * 1.4 + borderThickness * 2,
  885. round: parameters["round"],
  886. };
  887. // 1.4 is extra height factor for text below baseline: g,j,p,q.
  888. // context.fillRect(0, 0, painting.width, painting.height)
  889. this.roundRect(
  890. context,
  891. borderThickness / 2,
  892. borderThickness / 2,
  893. painting.width,
  894. painting.height,
  895. painting.round
  896. );
  897. // text color
  898. context.fillStyle =
  899. "rgba(" +
  900. fontColor.r +
  901. "," +
  902. fontColor.g +
  903. "," +
  904. fontColor.b +
  905. "," +
  906. fontColor.a +
  907. ")";
  908. context.textAlign = "center";
  909. context.textBaseline = "middle";
  910. context.fillText(message, painting.width / 2, painting.height / 2);
  911. // canvas contents will be used for a texture
  912. let texture = new THREE.Texture(canvas);
  913. texture.needsUpdate = true;
  914. let spriteMaterial = new THREE.SpriteMaterial({
  915. map: texture,
  916. useScreenCoordinates: false,
  917. depthTest: false, // 解决精灵谍影问题
  918. // blending: THREE.AdditiveBlending,
  919. // transparent: true,
  920. // alignment: spriteAlignment
  921. });
  922. let sprite = new THREE.Sprite(spriteMaterial);
  923. sprite.scale.set(1, 1 / 2, 1);
  924. return sprite;
  925. },
  926. // 城市 - 名称显示 - 样式
  927. roundRect(ctx, x, y, w, h, r) {
  928. ctx.beginPath();
  929. ctx.moveTo(x + r, y);
  930. ctx.lineTo(x + w - r, y);
  931. ctx.quadraticCurveTo(x + w, y, x + w, y + r);
  932. ctx.lineTo(x + w, y + h - r);
  933. ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
  934. ctx.lineTo(x + r, y + h);
  935. ctx.quadraticCurveTo(x, y + h, x, y + h - r);
  936. ctx.lineTo(x, y + r);
  937. ctx.quadraticCurveTo(x, y, x + r, y);
  938. ctx.closePath();
  939. ctx.fill();
  940. ctx.stroke();
  941. },
  942. // 告警 - 光波
  943. setAlarmWave(vector, color) {
  944. const geometry = new THREE.PlaneBufferGeometry(1, 1); //默认在XOY平面上
  945. const textureLoader = new THREE.TextureLoader(); // TextureLoader创建一个纹理加载器对象
  946. const texture = textureLoader.load("/static/map/texture/alarm.png");
  947. const material = new THREE.MeshBasicMaterial({
  948. color: color,
  949. map: texture,
  950. transparent: true, //使用背景透明的png贴图,注意开启透明计算
  951. opacity: 1.0,
  952. side: THREE.FrontSide,
  953. depthWrite: false, //禁止写入深度缓冲区数据
  954. blending: THREE.AdditiveBlending,
  955. });
  956. let mesh = new THREE.Mesh(geometry, material);
  957. mesh.position.set(vector.x, vector.y, vector.z);
  958. mesh.scale.set(0.4, 0.4, 0.4); // 设置mesh大小
  959. this.alarmWaveMeshArr.push(mesh);
  960. this.groupTwo.add(mesh);
  961. },
  962. // 告警 - 三角锥标记
  963. setAlarmCylinder(vector, color) {
  964. const geometry = new THREE.CylinderGeometry(0.1, 0.0, 0.3, 3);
  965. let material = new THREE.MeshPhongMaterial({
  966. // ShaderMaterial
  967. color: color,
  968. side: THREE.FrontSide,
  969. // blending: THREE.AdditiveBlending,
  970. transparent: true,
  971. opacity: 0.8,
  972. });
  973. const cylinder = new THREE.Mesh(geometry, material);
  974. cylinder.position.set(vector.x, vector.y, vector.z + 0.3);
  975. cylinder.rotateX(Math.PI / 2);
  976. cylinder.scale.set(1, 1, 1);
  977. this.alarmCylinderMeshArr.push(cylinder);
  978. this.groupTwo.add(cylinder);
  979. },
  980. // 告警 - 名称显示
  981. setAlarmName(vector, color, data) {
  982. // CSS2DRenderer生成的标签直接就是挂在真实的DOM上,并非是Vue的虚拟DOM上
  983. const div = document.createElement("div");
  984. div.className = "alarm-label";
  985. const icon = document.createElement("span");
  986. icon.className = "alarm-label-icon";
  987. icon.textContent = "●";
  988. icon.style.color = color;
  989. div.appendChild(icon);
  990. const text = document.createElement("span");
  991. text.className = "alarm-label-text";
  992. text.textContent = data.type;
  993. div.appendChild(text);
  994. const contentDiv = document.createElement("div");
  995. contentDiv.className = "alarm-label-content";
  996. contentDiv.innerHTML =
  997. "告警:" +
  998. data.content +
  999. "<br/>" +
  1000. "企业:" +
  1001. data.company +
  1002. "<br/>" +
  1003. "位置:" +
  1004. data.location +
  1005. "<br/>" +
  1006. "电话:" +
  1007. data.tel;
  1008. div.appendChild(contentDiv);
  1009. const label = new CSS2DObject(div);
  1010. label.position.set(vector.x, vector.y, vector.z + 0.65);
  1011. label.visible = false;
  1012. this.alarmNameMeshArr.push(label);
  1013. this.groupTwo.add(label);
  1014. // this.scene.add(spritey);
  1015. },
  1016. // 能源 - 光波
  1017. setEnergyWave(vector, color) {
  1018. const geometry = new THREE.PlaneBufferGeometry(1, 1); //默认在XOY平面上
  1019. const textureLoader = new THREE.TextureLoader(); // TextureLoader创建一个纹理加载器对象
  1020. const texture = textureLoader.load("/static/map/texture/alarm.png");
  1021. const material = new THREE.MeshBasicMaterial({
  1022. color: color,
  1023. map: texture,
  1024. transparent: true, //使用背景透明的png贴图,注意开启透明计算
  1025. opacity: 1.0,
  1026. side: THREE.FrontSide,
  1027. depthWrite: false, //禁止写入深度缓冲区数据
  1028. blending: THREE.AdditiveBlending,
  1029. });
  1030. let mesh = new THREE.Mesh(geometry, material);
  1031. mesh.position.set(vector.x, vector.y, vector.z);
  1032. mesh.scale.set(0.4, 0.4, 0.4); // 设置mesh大小
  1033. this.energyWaveMeshArr.push(mesh);
  1034. this.groupThree.add(mesh);
  1035. },
  1036. // 能源 - 三角锥标记
  1037. setEnergyCylinder(vector, color) {
  1038. const geometry = new THREE.CylinderGeometry(0.1, 0.0, 0.3, 20);
  1039. let material = new THREE.MeshPhongMaterial({
  1040. // ShaderMaterial
  1041. color: color,
  1042. side: THREE.FrontSide,
  1043. // blending: THREE.AdditiveBlending,
  1044. transparent: true,
  1045. opacity: 0.8,
  1046. });
  1047. const cylinder = new THREE.Mesh(geometry, material);
  1048. cylinder.position.set(vector.x, vector.y, vector.z + 0.3);
  1049. cylinder.rotateX(Math.PI / 2);
  1050. cylinder.scale.set(1, 1, 1);
  1051. this.energyCylinderMeshArr.push(cylinder);
  1052. this.groupThree.add(cylinder);
  1053. },
  1054. // 能源 - 名称显示
  1055. setEnergyName(vector, color, data) {
  1056. // CSS2DRenderer生成的标签直接就是挂在真实的DOM上,并非是Vue的虚拟DOM上
  1057. const div = document.createElement("div");
  1058. div.className = "alarm-label";
  1059. const icon = document.createElement("span");
  1060. icon.className = "alarm-label-icon";
  1061. icon.textContent = "◆";
  1062. icon.style.color = color;
  1063. div.appendChild(icon);
  1064. const text = document.createElement("span");
  1065. text.className = "alarm-label-text";
  1066. text.textContent = data.type;
  1067. div.appendChild(text);
  1068. const contentDiv = document.createElement("div");
  1069. contentDiv.className = "alarm-label-content";
  1070. contentDiv.innerHTML =
  1071. "告警:" +
  1072. data.content +
  1073. "<br/>" +
  1074. "企业:" +
  1075. data.company +
  1076. "<br/>" +
  1077. "位置:" +
  1078. data.location +
  1079. "<br/>" +
  1080. "电话:" +
  1081. data.tel;
  1082. div.appendChild(contentDiv);
  1083. const label = new CSS2DObject(div);
  1084. label.position.set(vector.x, vector.y, vector.z + 0.65);
  1085. label.visible = false;
  1086. this.energyNameMeshArr.push(label);
  1087. this.groupThree.add(label);
  1088. // this.scene.add(spritey);
  1089. },
  1090. // 监测 - 光波
  1091. setMonitorWave(vector) {
  1092. const geometry = new THREE.PlaneBufferGeometry(1, 1); //默认在XOY平面上
  1093. const textureLoader = new THREE.TextureLoader(); // TextureLoader创建一个纹理加载器对象
  1094. const texture = textureLoader.load("/static/map/texture/marker.png");
  1095. const material = new THREE.MeshBasicMaterial({
  1096. color: "#B3FFFF",
  1097. map: texture,
  1098. transparent: true, //使用背景透明的png贴图,注意开启透明计算
  1099. opacity: 0.9,
  1100. side: THREE.FrontSide,
  1101. depthWrite: false, //禁止写入深度缓冲区数据
  1102. // blending: THREE.AdditiveBlending,
  1103. });
  1104. let mesh = new THREE.Mesh(geometry, material);
  1105. mesh.position.set(vector.x, vector.y, vector.z);
  1106. mesh.scale.set(0.4, 0.4, 0.4); // 设置mesh大小
  1107. this.monitorWaveMeshArr.push(mesh);
  1108. this.groupFour.add(mesh);
  1109. },
  1110. // 监测 - 标记
  1111. setMonitorIcon(vector) {
  1112. const geometry = new THREE.PlaneGeometry(1, 1);
  1113. const texture = new THREE.TextureLoader().load(
  1114. "/static/map/texture/monitor.png"
  1115. );
  1116. let material = new THREE.MeshPhongMaterial({
  1117. map: texture,
  1118. // color: "#ffffff",
  1119. side: THREE.DoubleSide,
  1120. blending: THREE.AdditiveBlending,
  1121. transparent: true,
  1122. opacity: 0.9,
  1123. });
  1124. const mesh = new THREE.Mesh(geometry, material);
  1125. mesh.position.set(vector.x, vector.y, vector.z + 0.25);
  1126. mesh.rotateX(Math.PI / 4);
  1127. mesh.scale.set(0.3, 0.3, 0.3);
  1128. // mesh.lookAt(this.camera.position)
  1129. this.monitorIconMeshArr.push(mesh);
  1130. this.groupFour.add(mesh);
  1131. },
  1132. // 监测 - 名称显示
  1133. setMonitorName(vector, data) {
  1134. // CSS2DRenderer生成的标签直接就是挂在真实的DOM上,并非是Vue的虚拟DOM上
  1135. const div = document.createElement("div");
  1136. div.className = "alarm-label";
  1137. const icon = document.createElement("span");
  1138. icon.className = "alarm-label-icon";
  1139. icon.textContent = "◉";
  1140. icon.style.color = "#ffffff";
  1141. div.appendChild(icon);
  1142. const text = document.createElement("span");
  1143. text.className = "alarm-label-text";
  1144. text.textContent = data.monitor;
  1145. div.appendChild(text);
  1146. const contentDiv = document.createElement("div");
  1147. contentDiv.className = "alarm-label-content";
  1148. contentDiv.innerHTML =
  1149. "状态:" + data.content + "<br/>" + "位置:" + data.location;
  1150. div.appendChild(contentDiv);
  1151. const label = new CSS2DObject(div);
  1152. label.position.set(vector.x, vector.y, vector.z + 0.65);
  1153. label.visible = false;
  1154. this.monitorNameMeshArr.push(label);
  1155. this.groupFour.add(label);
  1156. // this.scene.add(spritey);
  1157. },
  1158. // 射线
  1159. setRaycaster() {
  1160. this.raycaster = new THREE.Raycaster();
  1161. this.mouse = new THREE.Vector2();
  1162. this.tooltip = document.getElementById("tooltip");
  1163. const onMouseMove = (event) => {
  1164. this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  1165. this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
  1166. this.tooltip.style.left = event.clientX + 2 + "px";
  1167. this.tooltip.style.top = event.clientY + 2 + "px";
  1168. };
  1169. // 点击地图事件
  1170. const onClick = (event) => {
  1171. // console.log(this.lastPick);
  1172. if (this.lastPick && "point" in this.lastPick)
  1173. this.mapClickTween(this.lastPick.point);
  1174. else this.resetCameraTween();
  1175. };
  1176. window.addEventListener("mousemove", onMouseMove, false);
  1177. window.addEventListener("click", onClick, false);
  1178. },
  1179. // 鼠标悬浮显示
  1180. showTip() {
  1181. // 显示省份的信息
  1182. if (this.lastPick) {
  1183. const properties = this.lastPick.object.parent.properties;
  1184. this.tooltip.textContent = properties.name;
  1185. this.tooltip.style.visibility = "visible";
  1186. } else {
  1187. this.tooltip.style.visibility = "hidden";
  1188. }
  1189. },
  1190. // 窗口变化
  1191. onWindowSize() {
  1192. // let container = document.getElementById("container");
  1193. this.camera.aspect =
  1194. this.container.clientWidth / this.container.clientHeight;
  1195. this.camera.updateProjectionMatrix();
  1196. this.renderer.setSize(
  1197. this.container.clientWidth,
  1198. this.container.clientHeight
  1199. );
  1200. this.labelRenderer.setSize(
  1201. this.container.clientWidth,
  1202. this.container.clientHeight
  1203. );
  1204. },
  1205. // Tween - 城市光柱动画
  1206. cityCylinderTween() {
  1207. this.cityCylinderMeshArr.forEach((mesh) => {
  1208. // console.log(mesh);
  1209. const begin = {
  1210. z: mesh.position.z,
  1211. };
  1212. const end = {
  1213. z: mesh.position.z + mesh._height,
  1214. };
  1215. const self = this;
  1216. this.$tween.use({
  1217. begin,
  1218. end,
  1219. time: 1000,
  1220. onUpdate(obj) {
  1221. mesh.position.z = obj.z;
  1222. },
  1223. onComplete() {
  1224. // 动画结束,显示数据
  1225. self.cityNumMeshArr.forEach((e) => {
  1226. e.visible = true;
  1227. });
  1228. // console.log(document.getElementsByClassName("city-num-label"));
  1229. // for (let i = 0; i < document.getElementsByClassName("city-num-label").length; i++) {
  1230. // document.getElementsByClassName("city-num-label")[i].style.display = "block"
  1231. // }
  1232. },
  1233. });
  1234. });
  1235. },
  1236. // Tween - 加载时相机移动动画
  1237. cameraTween(i) {
  1238. // console.log("cameraTween");
  1239. !i ? (i = 0) : (i = i);
  1240. if (i > this.cameraPosArr.length - 1) {
  1241. // this.cityCylinderTween();
  1242. return false;
  1243. }
  1244. //关闭控制器
  1245. this.controller.enabled = false;
  1246. const begin = {
  1247. x: this.camera.position.x,
  1248. y: this.camera.position.y,
  1249. z: this.camera.position.z,
  1250. };
  1251. const end = {
  1252. x: this.cameraPosArr[i].x,
  1253. y: this.cameraPosArr[i].y,
  1254. z: this.cameraPosArr[i].z,
  1255. // x: 0,
  1256. // y: -3.0,
  1257. // z: 3.8,
  1258. };
  1259. const self = this;
  1260. this.$tween.use({
  1261. begin,
  1262. end,
  1263. time: 1500,
  1264. onUpdate(obj) {
  1265. self.camera.position.x = obj.x;
  1266. self.camera.position.y = obj.y;
  1267. self.camera.position.z = obj.z;
  1268. // self.controller.target.x = obj.x;
  1269. // self.controller.target.y = obj.y;
  1270. // self.controller.target.z = obj.z;
  1271. // 控制器更新
  1272. self.controller.update();
  1273. },
  1274. onComplete() {
  1275. self.controller.enabled = true;
  1276. self.cameraTween(i + 1);
  1277. },
  1278. });
  1279. },
  1280. // Tween - 点击省份动画
  1281. mapClickTween(pos) {
  1282. //关闭控制器
  1283. this.controller.enabled = false;
  1284. const begin = {
  1285. x: this.camera.position.x,
  1286. y: this.camera.position.y,
  1287. z: this.camera.position.z,
  1288. };
  1289. const end = {
  1290. x: pos.x,
  1291. y: pos.y,
  1292. z: pos.z + 2.5,
  1293. };
  1294. const self = this;
  1295. this.$tween.use({
  1296. begin,
  1297. end,
  1298. time: 500,
  1299. onUpdate(obj) {
  1300. self.camera.position.x = obj.x;
  1301. self.camera.position.y = obj.y;
  1302. self.camera.position.z = obj.z;
  1303. self.camera.lookAt(obj.x, obj.y, obj.z);
  1304. // 控制器更新
  1305. self.controller.update();
  1306. },
  1307. onComplete() {
  1308. self.controller.enabled = true;
  1309. },
  1310. });
  1311. },
  1312. // Tween - 重置相机
  1313. resetCameraTween() {
  1314. //关闭控制器
  1315. this.controller.enabled = false;
  1316. const begin = {
  1317. x: this.camera.position.x,
  1318. y: this.camera.position.y,
  1319. z: this.camera.position.z,
  1320. };
  1321. const end = {
  1322. x: this.cameraPosArr[this.cameraPosArr.length - 1].x,
  1323. y: this.cameraPosArr[this.cameraPosArr.length - 1].y,
  1324. z: this.cameraPosArr[this.cameraPosArr.length - 1].z,
  1325. };
  1326. const self = this;
  1327. this.$tween.use({
  1328. begin,
  1329. end,
  1330. time: 500,
  1331. onUpdate(obj) {
  1332. self.camera.position.x = obj.x;
  1333. self.camera.position.y = obj.y;
  1334. self.camera.position.z = obj.z;
  1335. self.camera.lookAt(0, 0, 0);
  1336. // 控制器更新
  1337. self.controller.update();
  1338. },
  1339. onComplete() {
  1340. self.controller.enabled = true;
  1341. },
  1342. });
  1343. },
  1344. // 动画
  1345. animate() {
  1346. requestAnimationFrame(this.animate);
  1347. this.showTip();
  1348. this.animationMouseover();
  1349. // city
  1350. this.animationCityWave();
  1351. this.animationCityMarker();
  1352. this.animationCityCylinder();
  1353. this.animationCityEdgeLight();
  1354. // alarm
  1355. this.animationAlarmWave();
  1356. this.animationAlarmCylinder();
  1357. // energy
  1358. this.animationEnergyWave();
  1359. // monitor
  1360. this.animationMonitorWave();
  1361. this.controller.update();
  1362. this.renderer.render(this.scene, this.camera);
  1363. this.labelRenderer.render(this.scene, this.camera);
  1364. },
  1365. // 动画 - 鼠标悬浮动作
  1366. animationMouseover() {
  1367. // 通过摄像机和鼠标位置更新射线
  1368. this.raycaster.setFromCamera(this.mouse, this.camera);
  1369. // 计算物体和射线的焦点,与当场景相交的对象有那些
  1370. const intersects = this.raycaster.intersectObjects(
  1371. this.scene.children,
  1372. true // true,则同时也会检测所有物体的后代
  1373. );
  1374. // 恢复上一次清空的
  1375. if (this.lastPick) {
  1376. this.lastPick.object.material[0].color.set("#4161ff");
  1377. // this.lastPick.object.material[1].color.set('#00035d');
  1378. }
  1379. this.lastPick = null;
  1380. this.lastPick = intersects.find(
  1381. (item) => item.object.material && item.object.material.length === 2 // 选择map object
  1382. );
  1383. if (this.lastPick) {
  1384. this.lastPick.object.material[0].color.set("#00035d");
  1385. // this.lastPick.object.material[1].color.set('#00035d');
  1386. }
  1387. },
  1388. // 动画 - 城市光柱
  1389. animationCityCylinder() {
  1390. this.cityCylinderMeshArr.forEach((mesh) => {
  1391. // console.log(mesh);
  1392. // 着色器动作
  1393. // let viewVector = new THREE.Vector3().subVectors(this.camera.position, mesh.getWorldPosition());
  1394. // mesh.material.uniforms.viewVector.value = this.camera.position;
  1395. // mesh.translateY(0.05);
  1396. // mesh.position.z <= mesh._height * 2 ? mesh.position.z += 0.05 : "";
  1397. // mesh.scale.z <= 1 ? mesh.scale.z += 0.05 : "";
  1398. });
  1399. },
  1400. // 动画 - 城市光波
  1401. animationCityWave() {
  1402. // console.log(this.cityWaveMesh);
  1403. this.cityWaveMeshArr.forEach((mesh) => {
  1404. // console.log(mesh);
  1405. mesh.size += 0.005; // Math.random() / 100 / 2
  1406. let scale = mesh.size / 1;
  1407. mesh.scale.set(scale, scale, scale);
  1408. if (mesh.size <= 0.5) {
  1409. mesh.material.opacity = 1;
  1410. } else if (mesh.size > 0.5 && mesh.size <= 1) {
  1411. mesh.material.opacity = 1.0 - (mesh.size - 0.5) * 2; // 0.5以后开始加透明度直到0
  1412. } else if (mesh.size > 1 && mesh.size < 2) {
  1413. mesh.size = 0;
  1414. }
  1415. });
  1416. },
  1417. // 动画 - 城市标记
  1418. animationCityMarker() {
  1419. this.cityMarkerMeshArr.forEach((mesh) => {
  1420. // console.log(mesh);
  1421. mesh.rotation.z += 0.05;
  1422. });
  1423. },
  1424. // 动画 - 城市边缘流光
  1425. animationCityEdgeLight() {
  1426. if (
  1427. this.mapEdgeLightObj.lightOpacitys &&
  1428. this.mapEdgeLightObj.mapEdgePoints
  1429. ) {
  1430. if (
  1431. this.mapEdgeLightObj.lightCurrentPos >
  1432. this.mapEdgeLightObj.mapEdgePoints.length
  1433. ) {
  1434. this.mapEdgeLightObj.lightCurrentPos = 0;
  1435. }
  1436. this.mapEdgeLightObj.lightCurrentPos += this.mapEdgeLightObj.lightSpeed;
  1437. for (let i = 0; i < this.mapEdgeLightObj.lightSpeed; i++) {
  1438. this.mapEdgeLightObj.lightOpacitys[
  1439. (this.mapEdgeLightObj.lightCurrentPos - i) %
  1440. this.mapEdgeLightObj.mapEdgePoints.length
  1441. ] = 0;
  1442. }
  1443. for (let i = 0; i < 100; i++) {
  1444. this.mapEdgeLightObj.lightOpacitys[
  1445. (this.mapEdgeLightObj.lightCurrentPos + i) %
  1446. this.mapEdgeLightObj.mapEdgePoints.length
  1447. ] = i / 50 > 2 ? 2 : i / 50;
  1448. }
  1449. if (this.mapEdgeLightObj.lightOpacityGeometry) {
  1450. this.mapEdgeLightObj.lightOpacityGeometry.attributes.aOpacity.needsUpdate = true;
  1451. }
  1452. }
  1453. },
  1454. // 动画 - 告警光波
  1455. animationAlarmWave() {
  1456. // console.log(this.alarmWaveMeshArr);
  1457. this.alarmWaveMeshArr.forEach((mesh) => {
  1458. // console.log(mesh);
  1459. mesh.rotation.z -= 0.01;
  1460. });
  1461. },
  1462. // 动画 - 告警三角锥
  1463. animationAlarmCylinder() {
  1464. this.alarmCylinderMeshArr.forEach((mesh) => {
  1465. // console.log(mesh);
  1466. mesh.rotation.y += 0.03;
  1467. // if(mesh.position.z < 0.8) {
  1468. // mesh.position.z += 0.03;
  1469. // } else if(mesh.position.z > 1.2) {
  1470. // mesh.position.z -= 0.03;
  1471. // }
  1472. });
  1473. },
  1474. // 动画 - 能源光波
  1475. animationEnergyWave() {
  1476. this.energyWaveMeshArr.forEach((mesh) => {
  1477. // console.log(mesh);
  1478. mesh.rotation.z -= 0.01;
  1479. });
  1480. },
  1481. // 动画 - 监测光波
  1482. animationMonitorWave() {
  1483. this.monitorWaveMeshArr.forEach((mesh) => {
  1484. // console.log(mesh);
  1485. mesh.rotation.z += 0.03;
  1486. });
  1487. },
  1488. // 切换Group形态
  1489. groupOneChange() {
  1490. console.log("groupOneChange");
  1491. // CSS2DObject数据单独做处理
  1492. this.cityNumMeshArr.forEach((e) => {
  1493. e.visible = true;
  1494. });
  1495. this.alarmNameMeshArr.forEach((e) => {
  1496. e.visible = false;
  1497. });
  1498. this.energyNameMeshArr.forEach((e) => {
  1499. e.visible = false;
  1500. });
  1501. this.monitorNameMeshArr.forEach((e) => {
  1502. e.visible = false;
  1503. });
  1504. this.groupOne.visible = true;
  1505. this.groupTwo.visible = false;
  1506. this.groupThree.visible = false;
  1507. this.groupFour.visible = false;
  1508. },
  1509. groupTwoChange() {
  1510. console.log("groupTwoChange");
  1511. // CSS2DObject数据单独做处理
  1512. this.cityNumMeshArr.forEach((e) => {
  1513. e.visible = false;
  1514. });
  1515. this.alarmNameMeshArr.forEach((e) => {
  1516. e.visible = true;
  1517. });
  1518. this.energyNameMeshArr.forEach((e) => {
  1519. e.visible = false;
  1520. });
  1521. this.monitorNameMeshArr.forEach((e) => {
  1522. e.visible = false;
  1523. });
  1524. this.groupOne.visible = false;
  1525. this.groupTwo.visible = true;
  1526. this.groupThree.visible = false;
  1527. this.groupFour.visible = false;
  1528. },
  1529. groupThreeChange() {
  1530. console.log("groupThreeChange");
  1531. // CSS2DObject数据单独做处理
  1532. this.cityNumMeshArr.forEach((e) => {
  1533. e.visible = false;
  1534. });
  1535. this.alarmNameMeshArr.forEach((e) => {
  1536. e.visible = false;
  1537. });
  1538. this.energyNameMeshArr.forEach((e) => {
  1539. e.visible = true;
  1540. });
  1541. this.monitorNameMeshArr.forEach((e) => {
  1542. e.visible = false;
  1543. });
  1544. this.groupOne.visible = false;
  1545. this.groupTwo.visible = false;
  1546. this.groupThree.visible = true;
  1547. this.groupFour.visible = false;
  1548. },
  1549. groupFourChange() {
  1550. console.log("groupFourChange");
  1551. // CSS2DObject数据单独做处理
  1552. this.cityNumMeshArr.forEach((e) => {
  1553. e.visible = false;
  1554. });
  1555. this.alarmNameMeshArr.forEach((e) => {
  1556. e.visible = false;
  1557. });
  1558. this.energyNameMeshArr.forEach((e) => {
  1559. e.visible = false;
  1560. });
  1561. this.monitorNameMeshArr.forEach((e) => {
  1562. e.visible = true;
  1563. });
  1564. this.groupOne.visible = false;
  1565. this.groupTwo.visible = false;
  1566. this.groupThree.visible = false;
  1567. this.groupFour.visible = true;
  1568. },
  1569. },
  1570. };
  1571. </script>
  1572. <style>
  1573. #container {
  1574. position: absolute;
  1575. width: 100%;
  1576. height: 100%;
  1577. }
  1578. #tooltip {
  1579. position: absolute;
  1580. z-index: 2;
  1581. background: linear-gradient(180deg, #b0deff 0%, #2c4fdc 100%);
  1582. padding: 6px 10px;
  1583. color: #fff;
  1584. border: 2px solid #fae52d;
  1585. font-weight: bold;
  1586. font-size: 16px;
  1587. /* border-radius: 4px; */
  1588. visibility: hidden;
  1589. }
  1590. #cityName {
  1591. z-index: 2;
  1592. }
  1593. .button-group {
  1594. position: absolute;
  1595. left: 24px;
  1596. top: 24px;
  1597. z-index: 2;
  1598. }
  1599. /* 城市统计数据 */
  1600. .city-num-label {
  1601. width: 32px;
  1602. height: 32px;
  1603. line-height: 32px;
  1604. font-size: 16px;
  1605. font-weight: bold;
  1606. color: #ffffff;
  1607. border-radius: 100px;
  1608. background-color: rgba(192, 174, 12, 0.8);
  1609. box-shadow: 0px 0px 4px rgba(237, 230, 25, 0.5);
  1610. border: 2px solid rgba(237, 230, 25, 1);
  1611. /* font-family: 'PingFang SC'; */
  1612. text-align: center;
  1613. cursor: pointer;
  1614. /* opacity: 0.8; */
  1615. transition: all 0.1s linear;
  1616. }
  1617. .city-num-label:hover {
  1618. margin-top: -60px;
  1619. width: 300px;
  1620. min-height: 140px;
  1621. padding: 16px 16px;
  1622. text-align: left;
  1623. /* opacity: 1.0; */
  1624. border-radius: 4px;
  1625. box-shadow: 0px 0px 12px rgba(237, 230, 25, 0.9);
  1626. }
  1627. .city-num-label-content {
  1628. display: none;
  1629. font-size: 12px;
  1630. line-height: 24px;
  1631. color: #eeeeee;
  1632. }
  1633. .city-num-label:hover .city-num-label-content {
  1634. display: block;
  1635. }
  1636. /* 告警名称 */
  1637. .alarm-label {
  1638. min-width: 100px;
  1639. height: 32px;
  1640. line-height: 30px;
  1641. border-radius: 4px;
  1642. background: rgba(10, 26, 52, 0.8);
  1643. border: 1px solid #59aff9;
  1644. box-shadow: 0px 0px 4px rgba(3, 149, 255, 0.5);
  1645. text-align: center;
  1646. cursor: pointer;
  1647. transition: all 0.1s linear;
  1648. }
  1649. .alarm-label:hover {
  1650. margin-top: -60px;
  1651. width: 300px;
  1652. min-height: 140px;
  1653. padding: 16px 16px;
  1654. text-align: left;
  1655. /* opacity: 1.0; */
  1656. box-shadow: 0px 0px 12px rgba(3, 149, 255, 0.9);
  1657. }
  1658. .alarm-label-icon {
  1659. margin-right: 4px;
  1660. font-size: 22px;
  1661. }
  1662. .alarm-label-text {
  1663. font-size: 16px;
  1664. font-weight: bold;
  1665. color: #ffffff;
  1666. }
  1667. .alarm-label-content {
  1668. display: none;
  1669. font-size: 12px;
  1670. line-height: 24px;
  1671. color: #eeeeee;
  1672. }
  1673. .alarm-label:hover .alarm-label-content {
  1674. display: block;
  1675. }
  1676. </style>

需要引入的插件 

  1. import * as THREE from "three";
  2. import * as d3 from 'd3';
  3. import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
  4. import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js';

 data里的属性,把摄像机、场景、控制器、城市上的数据、城市上的模型,都放在这先声明一下,因为牵扯到很多模型、摄像机、动画的逻辑变化,所以放到这就相当于全局变量,后续用的话很方便。

  1. data() {
  2. return {
  3. camera: null,
  4. scene: null,
  5. renderer: null,
  6. labelRenderer: null,
  7. container: null,
  8. // mesh: null,
  9. controller: null,
  10. map: null,
  11. raycaster: null,
  12. mouse: null,
  13. tooltip: null,
  14. lastPick: null,
  15. mapEdgeLightObj: {
  16. mapEdgePoints: [],
  17. lightOpacityGeometry: null, // 单独把geometry提出来,动画用
  18. // 边缘流光参数
  19. lightSpeed: 3,
  20. lightCurrentPos: 0,
  21. lightOpacitys: null,
  22. },
  23. // 每个屏幕模型一组
  24. groupOne: new THREE.Group(),
  25. groupTwo: new THREE.Group(),
  26. groupThree: new THREE.Group(),
  27. groupFour: new THREE.Group(),
  28. // groupOne 统计信息
  29. cityWaveMeshArr: [],
  30. cityCylinderMeshArr: [],
  31. cityMarkerMeshArr: [],
  32. cityNumMeshArr: [],
  33. // groupTwo 告警信息
  34. alarmWaveMeshArr: [],
  35. alarmCylinderMeshArr: [],
  36. alarmNameMeshArr: [],
  37. // groupThree 能源
  38. energyWaveMeshArr: [],
  39. energyCylinderMeshArr: [],
  40. energyNameMeshArr: [],
  41. // groupFour 环境
  42. monitorWaveMeshArr: [],
  43. monitorIconMeshArr: [],
  44. monitorNameMeshArr: [],
  45. // 城市信息
  46. mapConfig: {
  47. deep: 0.2,
  48. },
  49. // 摄像机移动位置,初始:0, -5, 1
  50. cameraPosArr: [
  51. // {x: 0.0, y: -0.3, z: 1},
  52. // {x: 5.0, y: 5.0, z: 2},
  53. // {x: 3.0, y: 3.0, z: 2},
  54. // {x: 0, y: 5.0, z: 2},
  55. // {x: -2.0, y: 3.0, z: 1},
  56. {x: 0, y: -3.0, z: 3.8},
  57. ],
  58. // 数据 - 区县总数量
  59. dataTotal: [xxxxxx],
  60. dataAlarm: [xxxxxx],
  61. dataEnergy: [xxxxxx],
  62. dataMonitor: [xxxxxx],
  63. };
  64. },

 

注意,renderer渲染器初始化的时候,除了正常的WebGLRenderer,别忘了CSS2DRenderer(为了在图上显示html的label),没用过这种的小伙伴,也可以先看一下官方的three.js examples

其他如果有不明白的,可以把three的官方文档看一下three.js docs

 根据地图的json,用d3的墨卡托投影来绘制地图模型了。在这里从static里,加载山东淄博市的json数据(这种json格式,不了解的可以查一下,对绘制地图也有帮助)

  1. // 加载地图数据
  2. loadMapData() {
  3. const loader = new THREE.FileLoader();
  4. loader.load("/static/map/json/zibo.json", data => {
  5. const jsondata = JSON.parse(data);
  6. this.addMapGeometry(jsondata);
  7. })
  8. },
  9. // 地图模型
  10. addMapGeometry(jsondata) {
  11. // 初始化一个地图对象
  12. this.map = new THREE.Object3D();
  13. // 墨卡托投影转换
  14. const projection = d3
  15. .geoMercator()
  16. .center([118.2, 36.7]) // 淄博市
  17. // .scale(2000)
  18. .translate([0.2, 0.15]); // 根据地球贴图做轻微调整
  19. jsondata.features.forEach((elem) => {
  20. // 定一个省份3D对象
  21. const province = new THREE.Object3D();
  22. // 每个的 坐标 数组
  23. const coordinates = elem.geometry.coordinates;
  24. // 循环坐标数组
  25. coordinates.forEach((multiPolygon) => {
  26. multiPolygon.forEach((polygon) => {
  27. const shape = new THREE.Shape();
  28. const lineMaterial = new THREE.LineBasicMaterial({
  29. color: '#ffffff',
  30. // linewidth: 1,
  31. // linecap: 'round', //ignored by WebGLRenderer
  32. // linejoin: 'round' //ignored by WebGLRenderer
  33. });
  34. // const lineGeometry = new THREE.Geometry();
  35. // for (let i = 0; i < polygon.length; i++) {
  36. // const [x, y] = projection(polygon[i]);
  37. // if (i === 0) {
  38. // shape.moveTo(x, -y);
  39. // }
  40. // shape.lineTo(x, -y);
  41. // lineGeometry.vertices.push(new THREE.Vector3(x, -y, 3));
  42. // }
  43. const lineGeometry = new THREE.BufferGeometry();
  44. const pointsArray = new Array();
  45. for (let i = 0; i < polygon.length; i++) {
  46. const [x, y] = projection(polygon[i]);
  47. if (i === 0) {
  48. shape.moveTo(x, -y);
  49. }
  50. shape.lineTo(x, -y);
  51. pointsArray.push(new THREE.Vector3(x, -y, this.mapConfig.deep));
  52. // 做边缘流光效果,把所有点保存下来
  53. this.mapEdgeLightObj.mapEdgePoints.push([x, -y, this.mapConfig.deep]);
  54. }
  55. // console.log(pointsArray);
  56. lineGeometry.setFromPoints(pointsArray);
  57. const extrudeSettings = {
  58. depth: this.mapConfig.deep,
  59. bevelEnabled: false, // 对挤出的形状应用是否斜角
  60. };
  61. const geometry = new THREE.ExtrudeGeometry(
  62. shape,
  63. extrudeSettings
  64. );
  65. const material = new THREE.MeshPhongMaterial({
  66. color: '#4161ff',
  67. transparent: true,
  68. opacity: 0.4,
  69. side: THREE.FrontSide,
  70. // depthTest: true,
  71. });
  72. const material1 = new THREE.MeshLambertMaterial({
  73. color: '#10004a',
  74. transparent: true,
  75. opacity: 0.7,
  76. side: THREE.FrontSide,
  77. // wireframe: true
  78. });
  79. const mesh = new THREE.Mesh(geometry, [material, material1]);
  80. const line = new THREE.Line(lineGeometry, lineMaterial);
  81. // 将省份的属性 加进来
  82. province.properties = elem.properties;
  83. // 将城市信息放到模型中,后续做动画用
  84. if (elem.properties.centroid) {
  85. const [x, y] = projection(elem.properties.centroid) // uv映射坐标
  86. province.properties._centroid = [x, y]
  87. }
  88. // console.log(elem.properties);
  89. province.add(mesh);
  90. province.add(line);
  91. })
  92. })
  93. // province.scale.set(5, 5, 0);
  94. // province.position.set(0, 0, 0);
  95. // console.log(province);
  96. this.map.add(province);
  97. })
  98. this.setMapEdgeLight();
  99. this.setMapName();
  100. this.scene.add(this.map);
  101. // 获取数据后,加载模型
  102. this.getResponseData();
  103. },

注意:

1.d3.geoMercator().center([118.2, 36.7]) .translate([0.2, 0.15]),因为地球表面是一个plane模型,贴了一个真实的地图,所以有一些沟壑河流,要根据translate做轻微调整,使模型其更贴合。

 

2、lineGeometry.vertices在高版本的three库中已弃用,改用BufferGeometry了

3、在循环所有地图边界点的时候,保存到了mapEdgePoints中,后续做地图边缘流光效果的时候用的上

4、整体思路就是,把地图先绘制成一个平面,然后通过ExtrudeGeometry模型拉一个深度,这个地图再贴到地球表面这个plane模型上,就好了

 

接下来在边界加一圈流光效果 

  1. // 地图边缘流光效果
  2. setMapEdgeLight() {
  3. // console.log(this.mapEdgeLightObj.mapEdgePoints);
  4. let positions = new Float32Array(this.mapEdgeLightObj.mapEdgePoints.flat(1)); // 数组深度遍历扁平化
  5. // console.log(positions);
  6. this.mapEdgeLightObj.lightOpacityGeometry = new THREE.BufferGeometry();
  7. // 设置顶点
  8. this.mapEdgeLightObj.lightOpacityGeometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
  9. // 设置 粒子透明度为 0
  10. this.mapEdgeLightObj.lightOpacitys = new Float32Array(positions.length).map(() => 0);
  11. this.mapEdgeLightObj.lightOpacityGeometry.setAttribute("aOpacity", new THREE.BufferAttribute(this.mapEdgeLightObj.lightOpacitys, 1));
  12. // 顶点着色器
  13. const vertexShader = `
  14. attribute float aOpacity;
  15. uniform float uSize;
  16. varying float vOpacity;
  17. void main(){
  18. gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.0);
  19. gl_PointSize = uSize;
  20. vOpacity=aOpacity;
  21. }
  22. `
  23. // 片段着色器
  24. const fragmentShader = `
  25. varying float vOpacity;
  26. uniform vec3 uColor;
  27. float invert(float n){
  28. return 1.-n;
  29. }
  30. void main(){
  31. if(vOpacity <=0.2){
  32. discard;
  33. }
  34. vec2 uv=vec2(gl_PointCoord.x,invert(gl_PointCoord.y));
  35. vec2 cUv=2.*uv-1.;
  36. vec4 color=vec4(1./length(cUv));
  37. color*=vOpacity;
  38. color.rgb*=uColor;
  39. gl_FragColor=color;
  40. }
  41. `
  42. const material = new THREE.ShaderMaterial({
  43. vertexShader: vertexShader,
  44. fragmentShader: fragmentShader,
  45. transparent: true, // 设置透明
  46. // blending: THREE.AdditiveBlending,
  47. uniforms: {
  48. uSize: {
  49. value: 5.0
  50. },
  51. uColor: {
  52. value: new THREE.Color("#ffffff") // 光点颜色 fffb85
  53. }
  54. }
  55. })
  56. // material.blending = THREE.AdditiveBlending;
  57. const opacityPointsMesh = new THREE.Points(this.mapEdgeLightObj.lightOpacityGeometry, material);
  58. this.scene.add(opacityPointsMesh);
  59. },

这里的整体思路是,之前已经把边界的点保存下来了,点一个接一个的亮,就形成了好看的流光效果。

animationCityEdgeLight方法是在animate中的,每一帧画面如何动的,可以先理解一下,后面再讲。

地表的模型和贴图

  1. // 地球贴图纹理
  2. setEarth() {
  3. const geometry = new THREE.PlaneGeometry(14.0, 14.0);
  4. const texture = new THREE.TextureLoader().load('/static/map/texture/earth.jpg');
  5. const bumpTexture = new THREE.TextureLoader().load('/static/map/texture/earth.jpg');
  6. // texture.wrapS = THREE.RepeatWrapping; // 质地.包裹
  7. // texture.wrapT = THREE.RepeatWrapping;
  8. const material = new THREE.MeshPhongMaterial({
  9. map: texture, // 贴图
  10. bumpMap: bumpTexture,
  11. bumpScale: 0.05,
  12. // specularMap: texture,
  13. // specular: 0xffffff,
  14. // shininess: 1,
  15. // color: "#000000",
  16. side: THREE.FrontSide}
  17. );
  18. const earthPlane = new THREE.Mesh(geometry, material);
  19. this.scene.add(earthPlane);
  20. },

 

获取区县中心点这个方法,后续会用到很多次,各种模型的展示基本都要基于这个定位。 

  1. // 地区中心点 - 获取向量
  2. mapElem2Centroid(elem) {
  3. // 找到中心点
  4. const y = -elem.properties._centroid[1];
  5. const x = elem.properties._centroid[0];
  6. // 转化为二维坐标
  7. const vector = new THREE.Vector3(x, y, this.mapConfig.deep + 0.01);
  8. return vector;
  9. },

接下来我们看一下如何往地图上,添加数据上的模型,这里要提前讲一下,后台获取的数据我们是不确定的,地图就这么大,不可能根据数值无限放大、缩小模型,那样效果很不好,所以,在一开始我们就要把数据做【归一化】处理,顾名思义,就是把数据都放到0-1之间,再根据这个比例来定模型多大 

  1. // 数据归一化,映射到0-1区间 - 获取最大值
  2. getMaxV(distributionInfo) {
  3. let max = 0;
  4. for (let item of distributionInfo) {
  5. if (max < item.total) max = item.total;
  6. }
  7. return max;
  8. },
  9. // 数据归一化,映射到0-1区间 - 获取最小值
  10. getMinV(distributionInfo) {
  11. let min = 1000000;
  12. for (let item of distributionInfo) {
  13. if (min > item.total) min = item.total;
  14. }
  15. return min;
  16. },
  17. // 数据归一化,映射到0-1区间
  18. normalization(data, min, max) {
  19. let normalizationRatio = (data - min) / (max - min)
  20. return normalizationRatio
  21. },
  22. // GroupOne 添加模型
  23. addCityModel() {
  24. // 数据归一化
  25. const min = this.getMinV(this.dataTotal);
  26. const max = this.getMaxV(this.dataTotal);
  27. // 添加模型
  28. this.map.children.forEach((elem, index) => {
  29. // console.log(elem);
  30. // 满足数据条件 dataTotal
  31. if(this.dataTotal) {
  32. const vector = this.mapElem2Centroid(elem);
  33. this.dataTotal.forEach(d => {
  34. // 数据归一化,映射到0-1区间
  35. let num = this.normalization(d.total, min, max);
  36. // 判断区县
  37. if(d.name === elem.properties.name) {
  38. // 添加城市光波
  39. this.setCityWave(vector);
  40. // 添加城市标记
  41. this.setCityMarker(vector);
  42. // 添加城市光柱
  43. this.setCityCylinder(vector, num);
  44. // 添加城市数据
  45. this.setCityNum(vector, num, d);
  46. }
  47. })
  48. this.scene.add(this.groupOne);
  49. }
  50. })
  51. },

这里我们展示第一个tab的城市模型(其它tab的同理),这个tab里,用addCityModel这个方法里,循环把各种模型添加进去;

这个包含几种模型:城市光波(从城市中央扩散)、标记(自转)、光柱、数据,具体对照可以看一下下图,一目了然

 

  接下来,我们看下每类模型是怎么创建的

  1. // 城市 - 光柱
  2. setCityCylinder(vector, num) {
  3. const height = num;
  4. const geometry = new THREE.CylinderGeometry(0.08, 0.08, height, 20);
  5. // 顶点着色器
  6. const vertexShader = `
  7. uniform vec3 viewVector;
  8. varying float intensity;
  9. void main() {
  10. gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4( position, 1.0 );
  11. vec3 actual_normal = vec3(modelMatrix * vec4(normal, 0.0));
  12. intensity = pow(dot(normalize(viewVector), actual_normal), 3.0);
  13. }
  14. `
  15. // 片段着色器
  16. const fragmentShader = `
  17. varying float intensity;
  18. void main() {
  19. vec3 glow = vec3(246, 239, 0) * 3.0;
  20. gl_FragColor = vec4(glow, 1);
  21. }
  22. `
  23. let material = new THREE.MeshPhongMaterial({ // ShaderMaterial
  24. // uniforms: {
  25. // viewVector: this.camera.position
  26. // },
  27. // vertexShader: vertexShader,
  28. // fragmentShader: fragmentShader,
  29. color: "#ede619",
  30. side: THREE.FrontSide,
  31. blending: THREE.AdditiveBlending,
  32. transparent: true,
  33. // depthTest: false,
  34. precision: "mediump",
  35. // depthFunc: THREE.LessEqualDepth,
  36. opacity: 0.9,
  37. });
  38. const cylinder = new THREE.Mesh(geometry, material);
  39. cylinder.position.set(vector.x, vector.y, vector.z + height / 2);
  40. cylinder.rotateX(Math.PI / 2);
  41. cylinder.scale.set(1, 1, 1);
  42. // cylinder.position.z -= height / 2;
  43. // cylinder.translateY(-height);
  44. cylinder._height = height;
  45. // 法向量计算位置
  46. // let coordVec3 = vector.normalize();
  47. // // mesh默认在XOY平面上,法线方向沿着z轴new THREE.Vector3(0, 0, 1)
  48. // let meshNormal = new THREE.Vector3(0, 0, 0);
  49. // // 四元数属性,角度旋转,quaternion表示mesh的角度状态,setFromUnitVectors();计算两个向量之间构成的四元数值
  50. // cylinder.quaternion.setFromUnitVectors(meshNormal, coordVec3);
  51. this.cityCylinderMeshArr.push(cylinder);
  52. this.groupOne.add(cylinder);
  53. // this.scene.add(cylinder);
  54. },
  55. // 城市 - 光波
  56. setCityWave(vector) {
  57. const cityGeometry = new THREE.PlaneBufferGeometry(1, 1); //默认在XOY平面上
  58. const textureLoader = new THREE.TextureLoader(); // TextureLoader创建一个纹理加载器对象
  59. const texture = textureLoader.load('/static/map/texture/wave.png');
  60. // 如果不同mesh材质的透明度、颜色等属性同一时刻不同,材质不能共享
  61. const cityWaveMaterial = new THREE.MeshBasicMaterial({
  62. color: "#ede619", // 0x22ffcc
  63. map: texture,
  64. transparent: true, //使用背景透明的png贴图,注意开启透明计算
  65. opacity: 1.0,
  66. side: THREE.FrontSide, //双面可见
  67. depthWrite: false, //禁止写入深度缓冲区数据
  68. blending: THREE.AdditiveBlending,
  69. });
  70. let cityWaveMesh = new THREE.Mesh(cityGeometry, cityWaveMaterial);
  71. cityWaveMesh.position.set(vector.x, vector.y, vector.z);
  72. cityWaveMesh.size = 0;
  73. // cityWaveMesh.scale.set(0.1, 0.1, 0.1); // 设置mesh大小
  74. // 法向量计算位置
  75. // let coordVec3 = vector.normalize();
  76. // // mesh默认在XOY平面上,法线方向沿着z轴new THREE.Vector3(0, 0, 1)
  77. // let meshNormal = new THREE.Vector3(0, 0, 0);
  78. // // 四元数属性,角度旋转,quaternion表示mesh的角度状态,setFromUnitVectors();计算两个向量之间构成的四元数值
  79. // cityWaveMesh.quaternion.setFromUnitVectors(meshNormal, coordVec3);
  80. this.cityWaveMeshArr.push(cityWaveMesh);
  81. this.groupOne.add(cityWaveMesh);
  82. // 添加到场景中
  83. // this.scene.add(cityWaveMesh);
  84. },
  85. // 城市 - 标记
  86. setCityMarker(vector) {
  87. const cityGeometry = new THREE.PlaneBufferGeometry(0.3, 0.3); //默认在XOY平面上
  88. const textureLoader = new THREE.TextureLoader(); // TextureLoader创建一个纹理加载器对象
  89. const texture = textureLoader.load('/static/map/texture/marker.png');
  90. // 如果不同mesh材质的透明度、颜色等属性同一时刻不同,材质不能共享
  91. const cityMaterial = new THREE.MeshBasicMaterial({
  92. color: "#ffe000", // 0x22ffcc
  93. map: texture,
  94. transparent: true, //使用背景透明的png贴图,注意开启透明计算
  95. opacity: 1.0,
  96. side: THREE.FrontSide, //双面可见
  97. depthWrite: false, //禁止写入深度缓冲区数据
  98. blending: THREE.AdditiveBlending,
  99. });
  100. cityMaterial.blending = THREE.CustomBlending;
  101. cityMaterial.blendSrc = THREE.SrcAlphaFactor;
  102. cityMaterial.blendDst = THREE.DstAlphaFactor;
  103. cityMaterial.blendEquation = THREE.AddEquation;
  104. let cityMarkerMesh = new THREE.Mesh(cityGeometry, cityMaterial);
  105. cityMarkerMesh.position.set(vector.x, vector.y, vector.z);
  106. cityMarkerMesh.size = 0;
  107. // cityWaveMesh.scale.set(0.1, 0.1, 0.1); // 设置mesh大小
  108. this.cityMarkerMeshArr.push(cityMarkerMesh);
  109. this.groupOne.add(cityMarkerMesh);
  110. // 添加到场景中
  111. // this.scene.add(cityMarkerMesh);
  112. },
  113. // 城市 - 数据显示
  114. setCityNum(vector, num, data) {
  115. // CSS2DRenderer生成的标签直接就是挂在真实的DOM上,并非是Vue的虚拟DOM上
  116. const div = document.createElement('div');
  117. div.className = 'city-num-label';
  118. div.textContent = data.total;
  119. const contentDiv = document.createElement('div');
  120. contentDiv.className = 'city-num-label-content';
  121. contentDiv.innerHTML =
  122. '本区县共有窑炉企业 ' + data.total + ' 个。<br/>' +
  123. '介绍:' + data.brief
  124. ;
  125. div.appendChild(contentDiv);
  126. const label = new CSS2DObject(div);
  127. label.position.set(vector.x, vector.y, num + 0.5);
  128. label.visible = true;
  129. this.cityNumMeshArr.push(label);
  130. this.groupOne.add(label);
  131. // this.scene.add(spritey);
  132. },

鼠标悬浮到地图上,可以识别,可以显示label,这得益于three的raycaster

  1. // 射线
  2. setRaycaster() {
  3. this.raycaster = new THREE.Raycaster();
  4. this.mouse = new THREE.Vector2();
  5. this.tooltip = document.getElementById('tooltip');
  6. const onMouseMove = (event) => {
  7. this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  8. this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
  9. this.tooltip.style.left = event.clientX + 2 + 'px';
  10. this.tooltip.style.top = event.clientY + 2 + 'px';
  11. }
  12. // 点击地图事件
  13. const onClick = (event) => {
  14. // console.log(this.lastPick);
  15. if(this.lastPick && "point" in this.lastPick) this.mapClickTween(this.lastPick.point);
  16. else this.resetCameraTween();
  17. }
  18. window.addEventListener('mousemove', onMouseMove, false);
  19. window.addEventListener('click', onClick, false);
  20. },
  21. // 鼠标悬浮显示
  22. showTip() {
  23. // 显示省份的信息
  24. if (this.lastPick) {
  25. const properties = this.lastPick.object.parent.properties;
  26. this.tooltip.textContent = properties.name;
  27. this.tooltip.style.visibility = 'visible';
  28. } else {
  29. this.tooltip.style.visibility = 'hidden';
  30. }
  31. },
  32. // 窗口变化
  33. onWindowSize() {
  34. // let container = document.getElementById("container");
  35. this.camera.aspect = this.container.clientWidth / this.container.clientHeight;
  36. this.camera.updateProjectionMatrix();
  37. this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
  38. this.labelRenderer.setSize(this.container.clientWidth, this.container.clientHeight);
  39. },

地图点击有一些事件的触发,这就避免不了需要移动摄像机。

比如:点击区县,摄像机拉进;点击空白,摄像机归位。页面加载完成时,摄像机从地表移动到现在的位置

  1. // Tween - 加载时相机移动动画
  2. cameraTween(i) {
  3. // console.log("cameraTween");
  4. !i ? i = 0 : i = i;
  5. if(i > this.cameraPosArr.length - 1) {
  6. // this.cityCylinderTween();
  7. return false;
  8. }
  9. //关闭控制器
  10. this.controller.enabled = false;
  11. const begin = {
  12. x: this.camera.position.x,
  13. y: this.camera.position.y,
  14. z: this.camera.position.z,
  15. };
  16. const end = {
  17. x: this.cameraPosArr[i].x,
  18. y: this.cameraPosArr[i].y,
  19. z: this.cameraPosArr[i].z,
  20. // x: 0,
  21. // y: -3.0,
  22. // z: 3.8,
  23. };
  24. const self = this;
  25. this.$tween.use({
  26. begin,
  27. end,
  28. time: 1500,
  29. onUpdate(obj) {
  30. self.camera.position.x = obj.x;
  31. self.camera.position.y = obj.y;
  32. self.camera.position.z = obj.z;
  33. // self.controller.target.x = obj.x;
  34. // self.controller.target.y = obj.y;
  35. // self.controller.target.z = obj.z;
  36. // 控制器更新
  37. self.controller.update();
  38. },
  39. onComplete() {
  40. self.controller.enabled = true;
  41. self.cameraTween(i+1);
  42. }
  43. });
  44. },
  45. // Tween - 点击省份动画
  46. mapClickTween(pos) {
  47. //关闭控制器
  48. this.controller.enabled = false;
  49. const begin = {
  50. x: this.camera.position.x,
  51. y: this.camera.position.y,
  52. z: this.camera.position.z,
  53. };
  54. const end = {
  55. x: pos.x,
  56. y: pos.y,
  57. z: pos.z + 2.5,
  58. };
  59. const self = this;
  60. this.$tween.use({
  61. begin,
  62. end,
  63. time: 500,
  64. onUpdate(obj) {
  65. self.camera.position.x = obj.x;
  66. self.camera.position.y = obj.y;
  67. self.camera.position.z = obj.z;
  68. self.camera.lookAt(obj.x, obj.y, obj.z);
  69. // 控制器更新
  70. self.controller.update();
  71. },
  72. onComplete() {
  73. self.controller.enabled = true;
  74. }
  75. });
  76. },
  77. // Tween - 重置相机
  78. resetCameraTween() {
  79. //关闭控制器
  80. this.controller.enabled = false;
  81. const begin = {
  82. x: this.camera.position.x,
  83. y: this.camera.position.y,
  84. z: this.camera.position.z,
  85. };
  86. const end = {
  87. x: this.cameraPosArr[this.cameraPosArr.length - 1].x,
  88. y: this.cameraPosArr[this.cameraPosArr.length - 1].y,
  89. z: this.cameraPosArr[this.cameraPosArr.length - 1].z,
  90. };
  91. const self = this;
  92. this.$tween.use({
  93. begin,
  94. end,
  95. time: 500,
  96. onUpdate(obj) {
  97. self.camera.position.x = obj.x;
  98. self.camera.position.y = obj.y;
  99. self.camera.position.z = obj.z;
  100. self.camera.lookAt(0, 0, 0);
  101. // 控制器更新
  102. self.controller.update();
  103. },
  104. onComplete() {
  105. self.controller.enabled = true;
  106. }
  107. });
  108. },

动画,就会用到神库Tween了,之前我们也引入了。

看一下animation的方法,我们的光波、城市标记怎么动,都在这里了 

  1. // 动画
  2. animate() {
  3. requestAnimationFrame(this.animate);
  4. this.showTip();
  5. this.animationMouseover();
  6. // city
  7. this.animationCityWave();
  8. this.animationCityMarker();
  9. this.animationCityCylinder();
  10. this.animationCityEdgeLight();
  11. this.controller.update();
  12. this.renderer.render(this.scene, this.camera);
  13. this.labelRenderer.render(this.scene, this.camera);
  14. },
  1. // 动画 - 鼠标悬浮动作
  2. animationMouseover() {
  3. // 通过摄像机和鼠标位置更新射线
  4. this.raycaster.setFromCamera(this.mouse, this.camera)
  5. // 计算物体和射线的焦点,与当场景相交的对象有那些
  6. const intersects = this.raycaster.intersectObjects(
  7. this.scene.children,
  8. true // true,则同时也会检测所有物体的后代
  9. )
  10. // 恢复上一次清空的
  11. if (this.lastPick) {
  12. this.lastPick.object.material[0].color.set('#4161ff');
  13. // this.lastPick.object.material[1].color.set('#00035d');
  14. }
  15. this.lastPick = null;
  16. this.lastPick = intersects.find(
  17. (item) => item.object.material && item.object.material.length === 2 // 选择map object
  18. )
  19. if (this.lastPick) {
  20. this.lastPick.object.material[0].color.set('#00035d');
  21. // this.lastPick.object.material[1].color.set('#00035d');
  22. }
  23. },
  24. // 动画 - 城市光柱
  25. animationCityCylinder() {
  26. this.cityCylinderMeshArr.forEach(mesh => {
  27. // console.log(mesh);
  28. // 着色器动作
  29. // let viewVector = new THREE.Vector3().subVectors(this.camera.position, mesh.getWorldPosition());
  30. // mesh.material.uniforms.viewVector.value = this.camera.position;
  31. // mesh.translateY(0.05);
  32. // mesh.position.z <= mesh._height * 2 ? mesh.position.z += 0.05 : "";
  33. // mesh.scale.z <= 1 ? mesh.scale.z += 0.05 : "";
  34. })
  35. },
  36. // 动画 - 城市光波
  37. animationCityWave() {
  38. // console.log(this.cityWaveMesh);
  39. this.cityWaveMeshArr.forEach(mesh => {
  40. // console.log(mesh);
  41. mesh.size += 0.005; // Math.random() / 100 / 2
  42. let scale = mesh.size / 1;
  43. mesh.scale.set(scale, scale, scale);
  44. if(mesh.size <= 0.5) {
  45. mesh.material.opacity = 1;
  46. } else if (mesh.size > 0.5 && mesh.size <= 1) {
  47. mesh.material.opacity = 1.0 - (mesh.size - 0.5) * 2; // 0.5以后开始加透明度直到0
  48. } else if (mesh.size > 1 && mesh.size < 2) {
  49. mesh.size = 0;
  50. }
  51. })
  52. },
  53. // 动画 - 城市标记
  54. animationCityMarker() {
  55. this.cityMarkerMeshArr.forEach(mesh => {
  56. // console.log(mesh);
  57. mesh.rotation.z += 0.05;
  58. })
  59. },

 看一下tab点击有什么逻辑

  1. // 切换Group形态
  2. groupOneChange() {
  3. console.log("groupOneChange");
  4. // CSS2DObject数据单独做处理
  5. this.cityNumMeshArr.forEach(e => {e.visible = true});
  6. this.alarmNameMeshArr.forEach(e => {e.visible = false});
  7. this.energyNameMeshArr.forEach(e => {e.visible = false});
  8. this.monitorNameMeshArr.forEach(e => {e.visible = false});
  9. this.groupOne.visible = true;
  10. this.groupTwo.visible = false;
  11. this.groupThree.visible = false;
  12. this.groupFour.visible = false;
  13. },

到这里,就知道为什么要提前把tab的模型进行分组放了

 完毕,下一节介绍tween.js动画


原文作者:​编辑 ethanpu

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小桥流水78/article/detail/867826
推荐阅读
相关标签
  

闽ICP备14008679号