<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      [轉]Three.js做一個酷炫的城市展示可視化大屏

      【保姆進階級】Three.js做一個酷炫的城市展示可視化大屏  

      ethanpu      原文鏈接:https://blog.csdn.net/ethanpu/article/details/125691957

      hi,大家好,我是ethan。

      想記錄博客很久了,一直懶得開個頭,以前寫過全棧、java、寫過python、寫過前端,寫過安全、寫過互聯網,但是我還是更喜歡前端可視化,平時也喜歡研究一下可視化的技術,也是從d3、gis、threejs、echarts、hicharts、cesium一步步淌過來的,可視化方向的路還有很長,我覺得一些shader實在是好難....

      web3.0盛行,元宇宙也是跟前端密切相關的,也想學習一下unity、three.ar.js之類的,有想法的小伙伴可以一起溝通一下~

      言歸正傳,最近呢在做一個可視化大屏,當然要炫,畢竟領導喜歡,廢話不多說,先上預覽:

       

       

      bb185a2e-b902-48eb-91a6-5ea79eaf53c9



       


      bb185a2e-b902-48eb-91a6-5ea79eaf53c9

      分解代碼前,我們先介紹一些這里面有幾個技術點:
      
      
      1、d3.js通過投影把地圖數據的json映射到3維空間中,城市地圖的json下載我就不多講了,網上有很多教程,換成自己所需的城市就行;
      
      2、地圖上展示的數據展示的label,一開始用的sprite小精靈模型做的,但是會失真不清楚,后來換成了CSS2DRenderer這種方式,就相當于把html渲染到3維空間里,屢試不爽;
      
      3、為了達到“酷炫智能”效果,在一加載和點擊區縣的時候,做了camera的動畫(鏡頭移動、拉近),在這里就要在vue中引入tween.js了,tween做補間動畫,還是很好用的;
      
      4、地圖邊緣做了個流光效果,這個有很多厲害的博主介紹過,我是稍作了下修改;
      
      5、每切換一個tab,隱藏/顯示相應模型,所以把一組模型放到一組group里;
      

        

      接下來我們可以帶著上面幾個點,看代碼~!

      項目使用vue的框架,我們先來看看項目目錄、依賴都有哪些,其中引入elementUI就是為了用用里面的按鈕,不用自己寫了:

       

       

      (Menu.vue是測試了一個3D的菜單,跟此項目沒有關聯,可以先不用理會)

      {
      "name": "default",
      "version": "0.1.0",
      "private": true,
      "scripts": {
      "serve": "vue-cli-service serve",
      "build": "vue-cli-service build"
      },
      "dependencies": {
      "@tweenjs/tween.js": "^18.6.4",
      "core-js": "^2.6.5",
      "element-ui": "^2.15.8",
      "three": "^0.140.2",
      "vue": "^2.6.10"
      },
      "devDependencies": {
      "@vue/cli-plugin-babel": "^3.8.0",
      "@vue/cli-service": "^3.8.0",
      "d3": "^7.4.4"
      }
      }
      tween這個包不好在vue里面直接用,所以提前去下載好,然后還要在main.js里面做聲明
      
      import Vue from 'vue'
      import App from './App.vue'
      import ElementUI from 'element-ui';
      import 'element-ui/lib/theme-chalk/index.css';
      // 補間動畫
      import tween from "./utils/tween";
      
      Vue.use(ElementUI);
      Vue.use(tween);
      
      Vue.config.productionTip = false
      
      new Vue({
      render: h => h(App),
      }).$mount('#app')

       

      接下來,我們看一下主要的代碼Main.vue

      <template>
      <div>
      <div id="container"></div>
      <div id="tooltip"></div>
      
      <el-button-group class="button-group">
      <el-button type="" icon="" @click="groupOneChange">首頁總覽</el-button>
      <el-button type="" icon="" @click="groupTwoChange">應急管理</el-button>
      <el-button type="" icon="" @click="groupThreeChange">能源管理</el-button>
      <el-button type="" icon="" @click="groupFourChange">環境監測</el-button>
      <!-- <el-button type="" icon="">綜合能源監控中心</el-button> -->
      
      </el-button-group>
      </div>
      </template>
      

        

      其中:

      container塊是主要渲染3d畫布的div;

      tooltip是鼠標懸浮到區縣時顯示區縣名稱div;

      button-group是左上部分做tab切換的按鈕組(全篇引入了elementUI就在這用到了...)

      這是需要的組件,提前引入

      import * as THREE from "three";
      import * as d3 from 'd3';
      import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
      import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js';
      下面是放在data里的屬性,把攝像機、場景、控制器、城市上的數據、城市上的模型,都放在這先聲明一下,因為牽扯到很多模型、攝像機、動畫的邏輯變化,所以放到這就相當于全局變量,后續用的話都很方便。

      data() {
      return {
      camera: null,
      scene: null,
      renderer: null,
      labelRenderer: null,
      container: null,
      // mesh: null,
      controller: null,
      map: null,
      raycaster: null,
      mouse: null,
      tooltip: null,
      lastPick: null,
      mapEdgeLightObj: {
      mapEdgePoints: [],
      lightOpacityGeometry: null, // 單獨把geometry提出來,動畫用
      
      // 邊緣流光參數
      lightSpeed: 3,
      lightCurrentPos: 0,
      lightOpacitys: null,
      },
      
      // 每個屏幕模型一組
      groupOne: new THREE.Group(),
      groupTwo: new THREE.Group(),
      groupThree: new THREE.Group(),
      groupFour: new THREE.Group(),
      
      
      // groupOne 統計信息
      cityWaveMeshArr: [],
      cityCylinderMeshArr: [],
      cityMarkerMeshArr: [],
      cityNumMeshArr: [],
      
      // groupTwo 告警信息
      alarmWaveMeshArr: [],
      alarmCylinderMeshArr: [],
      alarmNameMeshArr: [],
      
      // groupThree 能源
      energyWaveMeshArr: [],
      energyCylinderMeshArr: [],
      energyNameMeshArr: [],
      
      // groupFour 環境
      monitorWaveMeshArr: [],
      monitorIconMeshArr: [],
      monitorNameMeshArr: [],
      
      // 城市信息
      mapConfig: {
      deep: 0.2,
      },
      // 攝像機移動位置,初始:0, -5, 1
      cameraPosArr: [
      // {x: 0.0, y: -0.3, z: 1},
      // {x: 5.0, y: 5.0, z: 2},
      // {x: 3.0, y: 3.0, z: 2},
      // {x: 0, y: 5.0, z: 2},
      // {x: -2.0, y: 3.0, z: 1},
      {x: 0, y: -3.0, z: 3.8},
      ],
      
      // 數據 - 區縣總數量
      dataTotal: [xxxxxx],
      dataAlarm: [xxxxxx],
      dataEnergy: [xxxxxx],
      dataMonitor: [xxxxxx],
      };
      },
      mounted函數不多說了,初始化什么的都放在這
      
      mounted() {
      this.init();
      this.animate();
      window.addEventListener('resize', this.onWindowSize)
      },
      著重看一下methods里面的方法,首先是把three的幾大基本元素初始化了
      
      //初始化
      init() {
      this.container = document.getElementById("container");
      this.setScene();
      this.setCamera();
      this.setRenderer(); // 創建渲染器對象
      this.setController(); // 創建控件對象
      this.addHelper();
      this.loadMapData();
      this.setEarth();
      this.setRaycaster();
      this.setLight();
      },
      
      setScene() {
      // 創建場景對象Scene
      this.scene = new THREE.Scene();
      },
      
      setCamera() {
      // 第二參數就是 長度和寬度比 默認采用瀏覽器 返回以像素為單位的窗口的內部寬度和高度
      this.camera = new THREE.PerspectiveCamera(
      75,
      window.innerWidth / window.innerHeight,
      0.1,
      500
      );
      
      this.camera.position.set(0, -5, 1); // 0, -5, 1
      this.camera.lookAt(new THREE.Vector3(0, 0, 0)); // 0, 0, 0 this.scene.position
      },
      
      setRenderer() {
      this.renderer = new THREE.WebGLRenderer({
      antialias: true,
      // logarithmicDepthBuffer: true, // 是否使用對數深度緩存
      });
      this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
      this.renderer.setPixelRatio(window.devicePixelRatio);
      // this.renderer.sortObjects = false; // 是否需要對對象排序
      this.container.appendChild(this.renderer.domElement);
      
      
      this.labelRenderer = new CSS2DRenderer();
      this.labelRenderer.setSize(this.container.clientWidth, this.container.clientHeight);
      this.labelRenderer.domElement.style.position = 'absolute';
      this.labelRenderer.domElement.style.top = 0;
      this.container.appendChild(this.labelRenderer.domElement);
      },
      
      setController() {
      this.controller = new OrbitControls(this.camera, this.labelRenderer.domElement);
      this.controller.minDistance = 2;
      this.controller.maxDistance = 5.5 // 5.5
      
      // 阻尼(慣性)
      // this.controller.enableDamping = true;
      // this.controller.dampingFactor = 0.04;
      
      this.controller.minAzimuthAngle = -Math.PI / 4;
      this.controller.maxAzimuthAngle = Math.PI / 4;
      
      this.controller.minPolarAngle = 1;
      this.controller.maxPolarAngle = Math.PI - 0.1;
      
      // 修改相機的lookAt是不會影響THREE.OrbitControls的target的
      // this.controller.target = new THREE.Vector3(0, -5, 2);
      
      },
      
      // 輔助線
      addHelper() {
      // let helper = new THREE.CameraHelper(this.camera);
      // this.scene.add(helper);
      
      //軸輔助 (每一個軸的長度)
      let axisHelper = new THREE.AxisHelper(150); // 紅線是X軸,綠線是Y軸,藍線是Z軸
      // this.scene.add(axisHelper);
      
      let gridHelper = new THREE.GridHelper(100, 30, 0x2C2C2C, 0x888888);
      // this.scene.add(gridHelper);
      },
      
      setLight() {
      const ambientLight = new THREE.AmbientLight(0x404040, 1.2);
      this.scene.add(ambientLight);
      // // 平行光
      const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
      this.scene.add(directionalLight);
      
      // 聚光光源 - 照模型
      // const spotLight = new THREE.SpotLight(0xffffff, 0.9);
      // spotLight.position.set(1, -4, 4);
      // spotLight.castShadow = true;
      // this.scene.add(spotLight);
      // 聚光光源輔助線
      // const spotLightHelper = new THREE.SpotLightHelper(spotLight);
      // this.scene.add(spotLightHelper);
      
      // 點光源 - 照模型
      const test = new THREE.PointLight("#ffffff", 1.8, 20);
      test.position.set(1, -7, 7);
      this.scene.add(test);
      const testHelperMap = new THREE.PointLightHelper(test);
      this.scene.add(testHelperMap);
      
      // 點光源 - 藍色照地球
      const pointLightMap = new THREE.PointLight("#4161ff", 1.4, 20);
      pointLightMap.position.set(0, 7, 3);
      this.scene.add(pointLightMap);
      const spotLightHelperMap = new THREE.PointLightHelper(pointLightMap);
      // this.scene.add(spotLightHelperMap);
      },

       

      這里需要注意,renderer渲染器初始化的時候,除了正常的WebGLRenderer,別忘了CSS2DRenderer(為了在圖上顯示html的label),沒用過這種的小伙伴,也可以先看一下官方的example:three.js examples

      其他如果有不明白的,可以把three的官方文檔看一下,在這就不過多說了

      three.js docs

      接下來就是根據地圖的json,用d3的墨卡托投影來繪制地圖模型了。在這里從static里,加載山東淄博市的json數據(這種json格式,不了解的可以查一下,對繪制地圖也有幫助)

      /

      / 加載地圖數據
      loadMapData() {
      const loader = new THREE.FileLoader();
      loader.load("/static/map/json/zibo.json", data => {
      const jsondata = JSON.parse(data);
      this.addMapGeometry(jsondata);
      })
      },
      
      // 地圖模型
      addMapGeometry(jsondata) {
      // 初始化一個地圖對象
      this.map = new THREE.Object3D();
      // 墨卡托投影轉換
      const projection = d3
      .geoMercator()
      .center([118.2, 36.7]) // 淄博市
      // .scale(2000)
      .translate([0.2, 0.15]); // 根據地球貼圖做輕微調整
      
      jsondata.features.forEach((elem) => {
      // 定一個省份3D對象
      const province = new THREE.Object3D();
      // 每個的 坐標 數組
      const coordinates = elem.geometry.coordinates;
      // 循環坐標數組
      coordinates.forEach((multiPolygon) => {
      multiPolygon.forEach((polygon) => {
      const shape = new THREE.Shape();
      const lineMaterial = new THREE.LineBasicMaterial({
      color: '#ffffff',
      // linewidth: 1,
      // linecap: 'round', //ignored by WebGLRenderer
      // linejoin: 'round' //ignored by WebGLRenderer
      });
      // const lineGeometry = new THREE.Geometry();
      // for (let i = 0; i < polygon.length; i++) {
      // const [x, y] = projection(polygon[i]);
      // if (i === 0) {
      // shape.moveTo(x, -y);
      // }
      // shape.lineTo(x, -y);
      // lineGeometry.vertices.push(new THREE.Vector3(x, -y, 3));
      // }
      const lineGeometry = new THREE.BufferGeometry();
      const pointsArray = new Array();
      for (let i = 0; i < polygon.length; i++) {
      const [x, y] = projection(polygon[i]);
      if (i === 0) {
      shape.moveTo(x, -y);
      }
      shape.lineTo(x, -y);
      pointsArray.push(new THREE.Vector3(x, -y, this.mapConfig.deep));
      
      // 做邊緣流光效果,把所有點保存下來
      this.mapEdgeLightObj.mapEdgePoints.push([x, -y, this.mapConfig.deep]);
      }
      // console.log(pointsArray);
      lineGeometry.setFromPoints(pointsArray);
      
      const extrudeSettings = {
      depth: this.mapConfig.deep,
      bevelEnabled: false, // 對擠出的形狀應用是否斜角
      };
      
      const geometry = new THREE.ExtrudeGeometry(
      shape,
      extrudeSettings
      );
      const material = new THREE.MeshPhongMaterial({
      color: '#4161ff',
      transparent: true,
      opacity: 0.4,
      side: THREE.FrontSide,
      // depthTest: true,
      });
      const material1 = new THREE.MeshLambertMaterial({
      color: '#10004a',
      transparent: true,
      opacity: 0.7,
      side: THREE.FrontSide,
      // wireframe: true
      });
      const mesh = new THREE.Mesh(geometry, [material, material1]);
      const line = new THREE.Line(lineGeometry, lineMaterial);
      // 將省份的屬性 加進來
      province.properties = elem.properties;
      
      // 將城市信息放到模型中,后續做動畫用
      if (elem.properties.centroid) {
      const [x, y] = projection(elem.properties.centroid) // uv映射坐標
      province.properties._centroid = [x, y]
      }
      
      // console.log(elem.properties);
      province.add(mesh);
      province.add(line);
      })
      })
      // province.scale.set(5, 5, 0);
      // province.position.set(0, 0, 0);
      // console.log(province);
      this.map.add(province);
      })
      this.setMapEdgeLight();
      this.setMapName();
      this.scene.add(this.map);
      
      // 獲取數據后,加載模型
      this.getResponseData();
      
      },

       


      這里需要注意幾點:

      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模型上,就ok了

      市區地圖的模型有了,接下來我們看下,如何在邊界加一圈流光效果

      // 地圖邊緣流光效果
      setMapEdgeLight() {
      // console.log(this.mapEdgeLightObj.mapEdgePoints);
      let positions = new Float32Array(this.mapEdgeLightObj.mapEdgePoints.flat(1)); // 數組深度遍歷扁平化
      // console.log(positions);
      this.mapEdgeLightObj.lightOpacityGeometry = new THREE.BufferGeometry();
      // 設置頂點
      this.mapEdgeLightObj.lightOpacityGeometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
      // 設置 粒子透明度為 0
      this.mapEdgeLightObj.lightOpacitys = new Float32Array(positions.length).map(() => 0);
      this.mapEdgeLightObj.lightOpacityGeometry.setAttribute("aOpacity", new THREE.BufferAttribute(this.mapEdgeLightObj.lightOpacitys, 1));

      // 頂點著色器
      const vertexShader = `
      attribute float aOpacity;
      uniform float uSize;
      varying float vOpacity;
      void main(){
      gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.0);
      gl_PointSize = uSize;
      vOpacity=aOpacity;
      }
      `
      // 片段著色器
      const fragmentShader = `
      varying float vOpacity;
      uniform vec3 uColor;
      float invert(float n){
      return 1.-n;
      }
      void main(){
      if(vOpacity <=0.2){
      discard;
      }
      vec2 uv=vec2(gl_PointCoord.x,invert(gl_PointCoord.y));
      vec2 cUv=2.*uv-1.;
      vec4 color=vec4(1./length(cUv));
      color*=vOpacity;
      color.rgb*=uColor;
      gl_FragColor=color;
      }
      `

      const material = new THREE.ShaderMaterial({
      vertexShader: vertexShader,
      fragmentShader: fragmentShader,
      transparent: true, // 設置透明
      // blending: THREE.AdditiveBlending,
      uniforms: {
      uSize: {
      value: 5.0
      },
      uColor: {
      value: new THREE.Color("#ffffff") // 光點顏色 fffb85
      }
      }
      })
      // material.blending = THREE.AdditiveBlending;
      const opacityPointsMesh = new THREE.Points(this.mapEdgeLightObj.lightOpacityGeometry, material);
      this.scene.add(opacityPointsMesh);

      },
      // 動畫 - 城市邊緣流光
      animationCityEdgeLight() {
      if(this.mapEdgeLightObj.lightOpacitys && this.mapEdgeLightObj.mapEdgePoints) {
      if (this.mapEdgeLightObj.lightCurrentPos > this.mapEdgeLightObj.mapEdgePoints.length) {
      this.mapEdgeLightObj.lightCurrentPos = 0;
      }

      this.mapEdgeLightObj.lightCurrentPos += this.mapEdgeLightObj.lightSpeed;
      for (let i = 0; i < this.mapEdgeLightObj.lightSpeed; i++) {
      this.mapEdgeLightObj.lightOpacitys[(this.mapEdgeLightObj.lightCurrentPos - i) % this.mapEdgeLightObj.mapEdgePoints.length] = 0;
      }

      for (let i = 0; i < 100; i++) {
      this.mapEdgeLightObj.lightOpacitys[(this.mapEdgeLightObj.lightCurrentPos + i) % this.mapEdgeLightObj.mapEdgePoints.length] = i / 50 > 2 ? 2 : i / 50;
      }

      if (this.mapEdgeLightObj.lightOpacityGeometry) {
      this.mapEdgeLightObj.lightOpacityGeometry.attributes.aOpacity.needsUpdate = true;
      }
      }
      },
      這里的整體思路是,之前已經把邊界的點保存下來了,點一個接一個的亮,就形成了好看的流光效果。

      animationCityEdgeLight方法是在animate中的,每一幀畫面如何動的,可以先理解一下,后期我們一起講。

      接下來我們看下地表的模型和貼圖

      // 地球貼圖紋理
      setEarth() {
      const geometry = new THREE.PlaneGeometry(14.0, 14.0);
      const texture = new THREE.TextureLoader().load('/static/map/texture/earth.jpg');
      const bumpTexture = new THREE.TextureLoader().load('/static/map/texture/earth.jpg');
      // texture.wrapS = THREE.RepeatWrapping; // 質地.包裹
      // texture.wrapT = THREE.RepeatWrapping;

      const material = new THREE.MeshPhongMaterial({
      map: texture, // 貼圖
      bumpMap: bumpTexture,
      bumpScale: 0.05,
      // specularMap: texture,
      // specular: 0xffffff,
      // shininess: 1,
      // color: "#000000",
      side: THREE.FrontSide}
      );
      const earthPlane = new THREE.Mesh(geometry, material);
      this.scene.add(earthPlane);
      },


      這里用了bumpTexture紋理,讓地表有那么一點點溝壑,這個可以調整一下自己感受一下

      地圖區縣的label

      // 地圖label
      setMapName(){
      this.map.children.forEach((elem, index) => {
      // 找到中心點
      const y = -elem.properties._centroid[1]
      const x = elem.properties._centroid[0]
      // 轉化為二維坐標
      const vector = new THREE.Vector3(x, y, this.mapConfig.deep + 0.01)

      // 添加城市名稱
      this.setCityName(vector, elem.properties.name);
      })
      },
      // 城市 - 名稱顯示
      setCityName(vector, name) {
      let spritey = this.makeTextSprite(
      name,
      {
      fontface: "微軟雅黑",
      fontsize: 28, // 定100調整位置,下面通過scale縮放
      fontColor: {r: 255, g: 255, b: 255, a: 1.0},
      borderColor: {r: 94, g: 94, b: 94, a: 0.0},
      backgroundColor: {r: 255, g: 255, b: 0, a: 0.0},
      borderThickness: 2,
      round: 6
      }
      );
      // 輕微偏移,錯開光柱
      spritey.position.set(vector.x + 0.06, vector.y + 0.0, 0.22); // num + 0.3
      this.scene.add(spritey);
      },

      // 城市 - 名稱顯示 - 小精靈mesh
      makeTextSprite(message, parameters) {
      if (parameters === undefined) parameters = {};

      let fontface = parameters["fontface"];
      let fontsize = parameters["fontsize"];
      let fontColor = parameters["fontColor"];
      let borderThickness = parameters["borderThickness"];
      let borderColor = parameters["borderColor"];
      let backgroundColor = parameters["backgroundColor"];

      // var spriteAlignment = THREE.SpriteAlignment.topLeft;

      let canvas = document.createElement('canvas');
      let context = canvas.getContext('2d');
      context.font = "Bold " + fontsize + "px " + fontface;

      // get size data (height depends only on font size)
      let metrics = context.measureText(message);
      let textWidth = metrics.width;

      // background color
      context.fillStyle = "rgba(" + backgroundColor.r + "," + backgroundColor.g + "," + backgroundColor.b + "," + backgroundColor.a + ")";
      // border color
      context.strokeStyle = "rgba(" + borderColor.r + "," + borderColor.g + "," + borderColor.b + "," + borderColor.a + ")";

      context.lineWidth = borderThickness;
      const painting = {
      width: textWidth * 1.4 + borderThickness * 2,
      height: fontsize * 1.4 + borderThickness * 2,
      round: parameters["round"]
      };
      // 1.4 is extra height factor for text below baseline: g,j,p,q.
      // context.fillRect(0, 0, painting.width, painting.height)
      this.roundRect(
      context,
      borderThickness / 2,
      borderThickness / 2,
      painting.width,
      painting.height,
      painting.round
      );

      // text color
      context.fillStyle = "rgba(" + fontColor.r + "," + fontColor.g + "," + fontColor.b + "," + fontColor.a + ")";
      context.textAlign = "center";
      context.textBaseline = "middle";

      context.fillText(message, painting.width / 2, painting.height / 2);

      // canvas contents will be used for a texture
      let texture = new THREE.Texture(canvas)
      texture.needsUpdate = true;
      let spriteMaterial = new THREE.SpriteMaterial({
      map: texture,
      useScreenCoordinates: false,
      depthTest: false, // 解決精靈諜影問題
      // blending: THREE.AdditiveBlending,
      // transparent: true,
      // alignment: spriteAlignment
      });
      let sprite = new THREE.Sprite(spriteMaterial);
      sprite.scale.set(1, 1 / 2, 1);
      return sprite;
      },
      // 城市 - 名稱顯示 - 樣式
      roundRect(ctx, x, y, w, h, r) {
      ctx.beginPath();
      ctx.moveTo(x+r, y);
      ctx.lineTo(x+w-r, y);
      ctx.quadraticCurveTo(x+w, y, x+w, y+r);
      ctx.lineTo(x+w, y+h-r);
      ctx.quadraticCurveTo(x+w, y+h, x+w-r, y+h);
      ctx.lineTo(x+r, y+h);
      ctx.quadraticCurveTo(x, y+h, x, y+h-r);
      ctx.lineTo(x, y+r);
      ctx.quadraticCurveTo(x, y, x+r, y);
      ctx.closePath();
      ctx.fill();
      ctx.stroke();
      },
      這里沒什么,因為要讓label每次都要沖著camera,就是用到了小精靈模型,然后手動canvas畫了下,不過感覺展示效果不好,但是也算個畫canvas的知識點了

      下面介紹一下,獲取區縣中心點這個方法,后續會用到很多次,各種模型的展示基本都要基于這個定位。

      // 地區中心點 - 獲取向量
      mapElem2Centroid(elem) {
      // 找到中心點
      const y = -elem.properties._centroid[1];
      const x = elem.properties._centroid[0];
      // 轉化為二維坐標
      const vector = new THREE.Vector3(x, y, this.mapConfig.deep + 0.01);
      return vector;
      },
      接下來我們看一下如何往地圖上,添加數據上的模型,這里要提前講一下,后臺獲取的數據我們是不確定的,地圖就這么大,不可能根據數值無限放大、縮小模型,那樣效果很不好,所以,在一開始我們就要把數據做【歸一化】處理,顧名思義,就是把數據都放到0-1之間,再根據這個比例來定模型多大

      // 數據歸一化,映射到0-1區間 - 獲取最大值
      getMaxV(distributionInfo) {
      let max = 0;
      for (let item of distributionInfo) {
      if (max < item.total) max = item.total;
      }
      return max;
      },
      // 數據歸一化,映射到0-1區間 - 獲取最小值
      getMinV(distributionInfo) {
      let min = 1000000;
      for (let item of distributionInfo) {
      if (min > item.total) min = item.total;
      }
      return min;
      },
      // 數據歸一化,映射到0-1區間
      normalization(data, min, max) {
      let normalizationRatio = (data - min) / (max - min)
      return normalizationRatio
      },

      // GroupOne 添加模型
      addCityModel() {
      // 數據歸一化
      const min = this.getMinV(this.dataTotal);
      const max = this.getMaxV(this.dataTotal);
      // 添加模型
      this.map.children.forEach((elem, index) => {
      // console.log(elem);
      // 滿足數據條件 dataTotal
      if(this.dataTotal) {
      const vector = this.mapElem2Centroid(elem);
      this.dataTotal.forEach(d => {
      // 數據歸一化,映射到0-1區間
      let num = this.normalization(d.total, min, max);

      // 判斷區縣
      if(d.name === elem.properties.name) {
      // 添加城市光波
      this.setCityWave(vector);

      // 添加城市標記
      this.setCityMarker(vector);

      // 添加城市光柱
      this.setCityCylinder(vector, num);

      // 添加城市數據
      this.setCityNum(vector, num, d);
      }
      })
      this.scene.add(this.groupOne);
      }
      })
      },
      這里我們展示第一個tab的城市模型(其它tab的同理),這個tab里,用addCityModel這個方法里,循環把各種模型添加進去;

      這個包含幾種模型:城市光波(從城市中央擴散)、標記(自轉)、光柱、數據,具體對照可以看一下下圖,一目了然

       


      wave

      marker
      接下來,我們看下每類模型是怎么創建的

      // 城市 - 光柱
      setCityCylinder(vector, num) {
      const height = num;
      const geometry = new THREE.CylinderGeometry(0.08, 0.08, height, 20);

      // 頂點著色器
      const vertexShader = `
      uniform vec3 viewVector;
      varying float intensity;
      void main() {
      gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4( position, 1.0 );
      vec3 actual_normal = vec3(modelMatrix * vec4(normal, 0.0));
      intensity = pow(dot(normalize(viewVector), actual_normal), 3.0);
      }
      `
      // 片段著色器
      const fragmentShader = `
      varying float intensity;
      void main() {
      vec3 glow = vec3(246, 239, 0) * 3.0;
      gl_FragColor = vec4(glow, 1);
      }
      `

      let material = new THREE.MeshPhongMaterial({ // ShaderMaterial
      // uniforms: {
      // viewVector: this.camera.position
      // },
      // vertexShader: vertexShader,
      // fragmentShader: fragmentShader,
      color: "#ede619",
      side: THREE.FrontSide,
      blending: THREE.AdditiveBlending,
      transparent: true,
      // depthTest: false,
      precision: "mediump",
      // depthFunc: THREE.LessEqualDepth,
      opacity: 0.9,
      });

      const cylinder = new THREE.Mesh(geometry, material);
      cylinder.position.set(vector.x, vector.y, vector.z + height / 2);
      cylinder.rotateX(Math.PI / 2);
      cylinder.scale.set(1, 1, 1);
      // cylinder.position.z -= height / 2;
      // cylinder.translateY(-height);
      cylinder._height = height;

      // 法向量計算位置
      // let coordVec3 = vector.normalize();
      // // mesh默認在XOY平面上,法線方向沿著z軸new THREE.Vector3(0, 0, 1)
      // let meshNormal = new THREE.Vector3(0, 0, 0);
      // // 四元數屬性,角度旋轉,quaternion表示mesh的角度狀態,setFromUnitVectors();計算兩個向量之間構成的四元數值
      // cylinder.quaternion.setFromUnitVectors(meshNormal, coordVec3);
      this.cityCylinderMeshArr.push(cylinder);
      this.groupOne.add(cylinder);
      // this.scene.add(cylinder);
      },

      // 城市 - 光波
      setCityWave(vector) {
      const cityGeometry = new THREE.PlaneBufferGeometry(1, 1); //默認在XOY平面上
      const textureLoader = new THREE.TextureLoader(); // TextureLoader創建一個紋理加載器對象
      const texture = textureLoader.load('/static/map/texture/wave.png');

      // 如果不同mesh材質的透明度、顏色等屬性同一時刻不同,材質不能共享
      const cityWaveMaterial = new THREE.MeshBasicMaterial({
      color: "#ede619", // 0x22ffcc
      map: texture,
      transparent: true, //使用背景透明的png貼圖,注意開啟透明計算
      opacity: 1.0,
      side: THREE.FrontSide, //雙面可見
      depthWrite: false, //禁止寫入深度緩沖區數據
      blending: THREE.AdditiveBlending,
      });

      let cityWaveMesh = new THREE.Mesh(cityGeometry, cityWaveMaterial);
      cityWaveMesh.position.set(vector.x, vector.y, vector.z);
      cityWaveMesh.size = 0;
      // cityWaveMesh.scale.set(0.1, 0.1, 0.1); // 設置mesh大小

      // 法向量計算位置
      // let coordVec3 = vector.normalize();
      // // mesh默認在XOY平面上,法線方向沿著z軸new THREE.Vector3(0, 0, 1)
      // let meshNormal = new THREE.Vector3(0, 0, 0);
      // // 四元數屬性,角度旋轉,quaternion表示mesh的角度狀態,setFromUnitVectors();計算兩個向量之間構成的四元數值
      // cityWaveMesh.quaternion.setFromUnitVectors(meshNormal, coordVec3);
      this.cityWaveMeshArr.push(cityWaveMesh);
      this.groupOne.add(cityWaveMesh);
      // 添加到場景中
      // this.scene.add(cityWaveMesh);
      },

      // 城市 - 標記
      setCityMarker(vector) {
      const cityGeometry = new THREE.PlaneBufferGeometry(0.3, 0.3); //默認在XOY平面上
      const textureLoader = new THREE.TextureLoader(); // TextureLoader創建一個紋理加載器對象
      const texture = textureLoader.load('/static/map/texture/marker.png');

      // 如果不同mesh材質的透明度、顏色等屬性同一時刻不同,材質不能共享
      const cityMaterial = new THREE.MeshBasicMaterial({
      color: "#ffe000", // 0x22ffcc
      map: texture,
      transparent: true, //使用背景透明的png貼圖,注意開啟透明計算
      opacity: 1.0,
      side: THREE.FrontSide, //雙面可見
      depthWrite: false, //禁止寫入深度緩沖區數據
      blending: THREE.AdditiveBlending,
      });
      cityMaterial.blending = THREE.CustomBlending;
      cityMaterial.blendSrc = THREE.SrcAlphaFactor;
      cityMaterial.blendDst = THREE.DstAlphaFactor;
      cityMaterial.blendEquation = THREE.AddEquation;

      let cityMarkerMesh = new THREE.Mesh(cityGeometry, cityMaterial);
      cityMarkerMesh.position.set(vector.x, vector.y, vector.z);
      cityMarkerMesh.size = 0;
      // cityWaveMesh.scale.set(0.1, 0.1, 0.1); // 設置mesh大小

      this.cityMarkerMeshArr.push(cityMarkerMesh);
      this.groupOne.add(cityMarkerMesh);
      // 添加到場景中
      // this.scene.add(cityMarkerMesh);
      },

      // 城市 - 數據顯示
      setCityNum(vector, num, data) {
      // CSS2DRenderer生成的標簽直接就是掛在真實的DOM上,并非是Vue的虛擬DOM上
      const div = document.createElement('div');
      div.className = 'city-num-label';
      div.textContent = data.total;

      const contentDiv = document.createElement('div');
      contentDiv.className = 'city-num-label-content';
      contentDiv.innerHTML =
      '本區縣共有窯爐企業 ' + data.total + ' 個。<br/>' +
      '介紹:' + data.brief
      ;
      div.appendChild(contentDiv);

      const label = new CSS2DObject(div);
      label.position.set(vector.x, vector.y, num + 0.5);
      label.visible = true;
      this.cityNumMeshArr.push(label);
      this.groupOne.add(label);
      // this.scene.add(spritey);

      },
      我們來講解一下每種模型的創建思路:

      1、光柱:就是圓柱體,然后附上效果,需要注意的是,圓柱體的高度怎么計算呢?記得我們剛才用的歸一函數嗎,就是在這里計算高度的。

      2、光波:一個透明png,貼到一個plane模型上,然后把融合模式改一下blending: THREE.AdditiveBlending。更多融合的效果,可以見官方例子 three.js examples

      3、標記:比較像光波,也是貼圖到plane上。

      4、數據:這里用到我們之前講的CSS2DRenderer,注意CSS2DRenderer生成的標簽直接就是掛在真實的DOM上,并非是Vue的虛擬DOM上。然后直接把樣式寫到css里,鼠標懸浮顯示,就用一個:hover,非常好用。

      這里還需要注意,因為這些模型都是tab 1里的,所以都放到groupOne這個變量里,后續做切換好用(替他tab里的模型同理)

      我們鼠標懸浮到地圖上,可以識別,可以顯示label,這得益于three的raycaster,簡單看一下代碼,很多博主已經講過了,這里就不過多贅述了。

      // 射線
      setRaycaster() {
      this.raycaster = new THREE.Raycaster();
      this.mouse = new THREE.Vector2();
      this.tooltip = document.getElementById('tooltip');
      const onMouseMove = (event) => {
      this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
      this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
      this.tooltip.style.left = event.clientX + 2 + 'px';
      this.tooltip.style.top = event.clientY + 2 + 'px';
      }

      // 點擊地圖事件
      const onClick = (event) => {
      // console.log(this.lastPick);
      if(this.lastPick && "point" in this.lastPick) this.mapClickTween(this.lastPick.point);
      else this.resetCameraTween();
      }

      window.addEventListener('mousemove', onMouseMove, false);
      window.addEventListener('click', onClick, false);

      },

      // 鼠標懸浮顯示
      showTip() {
      // 顯示省份的信息
      if (this.lastPick) {
      const properties = this.lastPick.object.parent.properties;

      this.tooltip.textContent = properties.name;

      this.tooltip.style.visibility = 'visible';
      } else {
      this.tooltip.style.visibility = 'hidden';
      }
      },

      // 窗口變化
      onWindowSize() {
      // let container = document.getElementById("container");
      this.camera.aspect = this.container.clientWidth / this.container.clientHeight;
      this.camera.updateProjectionMatrix();
      this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
      this.labelRenderer.setSize(this.container.clientWidth, this.container.clientHeight);
      },
      地圖點擊有一些事件的觸發,這就避免不了需要移動攝像機。

      比如:點擊區縣,攝像機拉進;點擊空白,攝像機歸位。頁面加載完成時,攝像機從地表移動到現在的位置(增加酷炫性,領導喜歡0.0)

      // Tween - 加載時相機移動動畫
      cameraTween(i) {
      // console.log("cameraTween");

      !i ? i = 0 : i = i;
      if(i > this.cameraPosArr.length - 1) {
      // this.cityCylinderTween();
      return false;
      }

      //關閉控制器
      this.controller.enabled = false;

      const begin = {
      x: this.camera.position.x,
      y: this.camera.position.y,
      z: this.camera.position.z,
      };
      const end = {
      x: this.cameraPosArr[i].x,
      y: this.cameraPosArr[i].y,
      z: this.cameraPosArr[i].z,
      // x: 0,
      // y: -3.0,
      // z: 3.8,
      };
      const self = this;
      this.$tween.use({
      begin,
      end,
      time: 1500,
      onUpdate(obj) {
      self.camera.position.x = obj.x;
      self.camera.position.y = obj.y;
      self.camera.position.z = obj.z;

      // self.controller.target.x = obj.x;
      // self.controller.target.y = obj.y;
      // self.controller.target.z = obj.z;

      // 控制器更新
      self.controller.update();
      },
      onComplete() {
      self.controller.enabled = true;
      self.cameraTween(i+1);
      }
      });
      },

      // Tween - 點擊省份動畫
      mapClickTween(pos) {
      //關閉控制器
      this.controller.enabled = false;

      const begin = {
      x: this.camera.position.x,
      y: this.camera.position.y,
      z: this.camera.position.z,
      };
      const end = {
      x: pos.x,
      y: pos.y,
      z: pos.z + 2.5,
      };
      const self = this;
      this.$tween.use({
      begin,
      end,
      time: 500,
      onUpdate(obj) {
      self.camera.position.x = obj.x;
      self.camera.position.y = obj.y;
      self.camera.position.z = obj.z;

      self.camera.lookAt(obj.x, obj.y, obj.z);

      // 控制器更新
      self.controller.update();
      },
      onComplete() {
      self.controller.enabled = true;
      }
      });
      },

      // Tween - 重置相機
      resetCameraTween() {
      //關閉控制器
      this.controller.enabled = false;

      const begin = {
      x: this.camera.position.x,
      y: this.camera.position.y,
      z: this.camera.position.z,
      };
      const end = {
      x: this.cameraPosArr[this.cameraPosArr.length - 1].x,
      y: this.cameraPosArr[this.cameraPosArr.length - 1].y,
      z: this.cameraPosArr[this.cameraPosArr.length - 1].z,
      };
      const self = this;
      this.$tween.use({
      begin,
      end,
      time: 500,
      onUpdate(obj) {
      self.camera.position.x = obj.x;
      self.camera.position.y = obj.y;
      self.camera.position.z = obj.z;

      self.camera.lookAt(0, 0, 0);

      // 控制器更新
      self.controller.update();
      },
      onComplete() {
      self.controller.enabled = true;
      }
      });
      },
      動畫,就會用到神庫Tween了,之前我們也引入了。

      需要著重注意的一點,在camera運動的時候,一定把控制器給關了,要不會...

      this.controller.enabled = false;

      然后別的也沒什么了,一個begin、一個end,動就完事了

      最后我們看一下animation的方法,我們的光波、城市標記怎么動,都在這里了

      // 動畫
      animate() {
      requestAnimationFrame(this.animate);

      this.showTip();
      this.animationMouseover();

      // city
      this.animationCityWave();
      this.animationCityMarker();
      this.animationCityCylinder();
      this.animationCityEdgeLight();


      this.controller.update();
      this.renderer.render(this.scene, this.camera);
      this.labelRenderer.render(this.scene, this.camera);
      },
      // 動畫 - 鼠標懸浮動作
      animationMouseover() {
      // 通過攝像機和鼠標位置更新射線
      this.raycaster.setFromCamera(this.mouse, this.camera)
      // 計算物體和射線的焦點,與當場景相交的對象有那些
      const intersects = this.raycaster.intersectObjects(
      this.scene.children,
      true // true,則同時也會檢測所有物體的后代
      )
      // 恢復上一次清空的
      if (this.lastPick) {
      this.lastPick.object.material[0].color.set('#4161ff');
      // this.lastPick.object.material[1].color.set('#00035d');
      }
      this.lastPick = null;
      this.lastPick = intersects.find(
      (item) => item.object.material && item.object.material.length === 2 // 選擇map object
      )
      if (this.lastPick) {
      this.lastPick.object.material[0].color.set('#00035d');
      // this.lastPick.object.material[1].color.set('#00035d');
      }
      },

      // 動畫 - 城市光柱
      animationCityCylinder() {

      this.cityCylinderMeshArr.forEach(mesh => {
      // console.log(mesh);

      // 著色器動作
      // let viewVector = new THREE.Vector3().subVectors(this.camera.position, mesh.getWorldPosition());
      // mesh.material.uniforms.viewVector.value = this.camera.position;

      // mesh.translateY(0.05);
      // mesh.position.z <= mesh._height * 2 ? mesh.position.z += 0.05 : "";

      // mesh.scale.z <= 1 ? mesh.scale.z += 0.05 : "";

      })
      },

      // 動畫 - 城市光波
      animationCityWave() {
      // console.log(this.cityWaveMesh);
      this.cityWaveMeshArr.forEach(mesh => {
      // console.log(mesh);
      mesh.size += 0.005; // Math.random() / 100 / 2
      let scale = mesh.size / 1;
      mesh.scale.set(scale, scale, scale);
      if(mesh.size <= 0.5) {
      mesh.material.opacity = 1;
      } else if (mesh.size > 0.5 && mesh.size <= 1) {
      mesh.material.opacity = 1.0 - (mesh.size - 0.5) * 2; // 0.5以后開始加透明度直到0
      } else if (mesh.size > 1 && mesh.size < 2) {
      mesh.size = 0;
      }
      })
      },
      // 動畫 - 城市標記
      animationCityMarker() {
      this.cityMarkerMeshArr.forEach(mesh => {
      // console.log(mesh);
      mesh.rotation.z += 0.05;
      })
      },
      本來光柱做的是從地上慢慢上升的,后來為了做其他邏輯屏蔽了,直接就立在那了...

      這里著重看一下城市光波:它是從中心開始慢慢擴大,到一定條件是慢慢透明度變為0。

      最后,看一下tab點擊有什么邏輯吧

      // 切換Group形態
      groupOneChange() {
      console.log("groupOneChange");
      // CSS2DObject數據單獨做處理
      this.cityNumMeshArr.forEach(e => {e.visible = true});
      this.alarmNameMeshArr.forEach(e => {e.visible = false});
      this.energyNameMeshArr.forEach(e => {e.visible = false});
      this.monitorNameMeshArr.forEach(e => {e.visible = false});

      this.groupOne.visible = true;
      this.groupTwo.visible = false;
      this.groupThree.visible = false;
      this.groupFour.visible = false;

      },
      到這里,就知道為什么要提前把tab的模型進行分組放了

      好啦,到這里就介紹完了,

      如果有問題!

      如果你也喜歡前端!

      如果你也喜歡可視化!

      如果你也喜歡3D世界!

      歡迎評論區和私信交流~

      最后附上代碼,有需要的小伙伴可以一鍵run起來哦(覺得有用就star一下哦~)

      GitHub - puyeyu/ThreeJs-Earth

      posted @ 2022-09-14 16:44  柒零壹  閱讀(3011)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 欧美亚洲另类自拍偷在线拍| 精品天堂色吊丝一区二区| 亚洲日韩成人无码不卡网站| 日韩精品一区二区三区中文| 97人人添人人澡人人澡人人澡| 99热精品国产三级在线观看| 国产精品店无码一区二区三区 | 影音先锋啪啪av资源网站| 国产精品爱久久久久久久电影| 日韩精品一区二区亚洲专区| 人人妻人人狠人人爽| 中文字幕久久国产精品| 亚洲欧洲一区二区精品| 亚洲 制服 丝袜 无码| 国产精品无码无需播放器| 久久精品不卡一区二区| 日韩精品国产二区三区 | 四虎影视库国产精品一区| 亚洲少妇人妻无码视频| 国产精品黄色精品黄色大片 | 大地资源高清免费观看| 伊人天天久大香线蕉av色| 久久精品国产99久久久古代| 国产精品嫩草99av在线| 91精品国产91热久久久久福利| 玩弄漂亮少妇高潮白浆| 国产中年熟女高潮大集合| 免费久久人人爽人人爽AV| 欧美寡妇xxxx黑人猛交| 日韩精品一区二区三区四| 久久在线视频免费观看| 91密桃精品国产91久久| 久久人与动人物a级毛片| 欧美牲交videossexeso欧美| 久久久久青草线蕉综合超碰| 无套内谢少妇一二三四| 亚洲人成网站在线观看播放不卡| 亚洲色大成网站www看下面| 中文字幕日韩人妻一区| 久久人妻av无码中文专区| 色成人亚洲|