資料視覺化【原創】vue+arcgis+threejs 實現立體光圈閃爍效果

2023-09-05 18:00:27

本文適合對vue,arcgis4.x,threejs,ES6較熟悉的人群食用。

效果圖:

 素材:

 

主要思路:

先用arcgis externalRenderers封裝了一個ExternalRendererLayer,在裡面把arcgis和threejs的context關聯,然後再寫個子類繼承它,這部分類容在上一個貼文裡面有講過。

子類FlashLayer繼承它,並在裡面封裝了一個excute方法用來執行閃爍,引數包括point和height。先構建一個管道CatmullRomCurve3,在構建環RingGeometry,然後在updateModels裡面去更新管道的scale.z,更新環的半徑和透明度。

excute方法:

 1 excute(point, height) {
 2         // let pointList = [
 3         //     [114.31456780904838, 30.55355011036358, 0],
 4         //     [114.31456780904838, 30.55355011036358, 2000]
 5         // ];
 6         let pointList = [
 7             [
 8                 point.longitude,
 9                 point.latitude,
10                 0
11             ],
12             [
13                 point.longitude,
14                 point.latitude,
15                 height
16             ]
17         ]
18 
19         let linePoints = [];
20         //確定幾何體位置
21         pointList.forEach((item) => {
22             var renderLinePoints = this.lngLatToXY(this.view, [item[0], item[1], item[2]]);
23             linePoints.push(new THREE.Vector3(renderLinePoints.vector3List.x, renderLinePoints
24                 .vector3List.y, renderLinePoints.vector3List.z));
25         })
26 
27         const lineImg = require('../../../../public/static/img/line.png')
28         let lineTexture = new THREE.TextureLoader().load(lineImg)
29         lineTexture.wrapS = lineTexture.wrapT = THREE.RepeatWrapping; //每個都重複
30         lineTexture.repeat.set(1, 1)
31         lineTexture.needsUpdate = true
32 
33         let lineMaterial = new THREE.MeshBasicMaterial({
34             map: lineTexture,
35             side: THREE.DoubleSide,
36             transparent: true,
37             opacity: 1
38         })
39 
40         // CatmullRomCurve3建立一條平滑的三維樣條曲線
41         let curve = new THREE.CatmullRomCurve3(linePoints) // 曲線路徑
42 
43         // 建立管道
44         let tubeGeometry = new THREE.TubeGeometry(curve, 1, 16)
45 
46         let mesh = new THREE.Mesh(tubeGeometry, lineMaterial);
47         mesh.name = 'FlashLayer_Line';
48         mesh.scale.z = 0;
49         mesh.layers.set(0);
50         this.group.add(mesh);
51 
52         //光圈
53         const ringUserData = {
54             width: 300,
55             innerRadius: 0,
56             opacity: 1,
57             opacityFlag: false,
58             per: 10,
59             innerMax: 800,
60             times: 0,
61             line: mesh
62         };
63         const ringGeometry = new THREE.RingGeometry(ringUserData.innerRadius, ringUserData.innerRadius +
64             ringUserData.width, 32);
65         const ringMaterial = new THREE.MeshPhongMaterial({
66             color: COLOR_RING,
67             emissive: COLOR_RING,
68             side: THREE.DoubleSide,
69             flatShading: true,
70             wireframe: false,
71             transparent: true,
72             opacity: ringUserData.opacity
73         });
74         const ringMesh = new THREE.Mesh(ringGeometry, ringMaterial);
75         const ringPoint = linePoints[1];
76         console.log(ringPoint);
77         ringMesh.position.set(ringPoint.x, ringPoint.y, ringPoint.z / 2 + (Math.random() * ringPoint.z / 4 | 0) );
78         ringMesh.name = 'FlashLayer_Ring';
79         ringMesh.userData = ringUserData;
80         ringMesh.layers.set(0);
81 
82         this.group.add(ringMesh);
83     }

 

updateModels方法(記得一定要銷燬哦):

 

  1 updateModels(context) {
  2         super.updateModels(context)
  3 
  4         if (this.group.children.length) {
  5             let rubbish = [];
  6             for (let i = this.group.children.length - 1; i >= 0; i--) {
  7                 const mesh = this.group.children[i];
  8                 if (mesh.name === 'FlashLayer_Line') {
  9                     mesh.material.map.offset.x -= 0.02;
 10                     mesh.scale.z += 0.03;
 11                     if(mesh.scale.z >= 1) {
 12                         mesh.scale.z = 1;
 13                     }
 14                     continue;
 15                 }
 16                 if (mesh.name === 'FlashLayer_Ring') {
 17                     const ringUserData = mesh.userData;
 18                     const per = ringUserData.per;
 19                     ringUserData.innerRadius += per;
 20                     ringUserData.opacity -= 1 / (ringUserData.innerMax / per);
 21 
 22                     if (ringUserData.innerRadius > ringUserData.innerMax) {
 23                         ringUserData.innerRadius = 0;
 24                         ringUserData.opacity = 1;
 25                         ringUserData.times++;
 26                         
 27                         if(ringUserData.times === FLASH_TIMES) {
 28                             rubbish.push(mesh)
 29                             continue;
 30                         }
 31                     }
 32 
 33                     // if(ringUserData.opacityFlag) {
 34                     //     ringUserData.opacity += 1 / 800 / 5;
 35                     // } else {
 36                     //     ringUserData.opacity -= 1 / 800 / 5;
 37                     // }
 38 
 39                     // if(ringUserData.opacity >= 1) {
 40                     //     ringUserData.opacity = 1;
 41                     //     ringUserData.opacityFlag = false;
 42                     // }
 43                     // if(ringUserData.opacity <= 0) {
 44                     //     ringUserData.opacity = 0;
 45                     //     ringUserData.opacityFlag = true;
 46                     // }
 47 
 48                     const ringGeometry = new THREE.RingGeometry(ringUserData.innerRadius, ringUserData.innerRadius +
 49                         ringUserData.width, 32);
 50                     const ringMaterial = new THREE.MeshPhongMaterial({
 51                         color: COLOR_RING,
 52                         emissive: COLOR_RING,
 53                         side: THREE.DoubleSide,
 54                         flatShading: true,
 55                         wireframe: false,
 56                         transparent: true,
 57                         opacity: ringUserData.opacity
 58                     });
 59                     mesh.geometry.dispose();
 60                     mesh.material.dispose();
 61                     mesh.geometry = ringGeometry;
 62                     mesh.material = ringMaterial;
 63                     mesh.rotation.z += 1;
 64 
 65                     continue;
 66                 }
 67             }
 68             
 69             if(rubbish.length) {
 70                 for(let i = 0; i < rubbish.length; i++) {
 71                     let rubbishMesh = rubbish[i];
 72                     let line = rubbishMesh.userData.line;
 73                     this.group.remove(line);
 74                     line.material.dispose();
 75                     line.geometry.dispose();
 76                     // line.dispose();
 77                     line = null;
 78                     this.group.remove(rubbishMesh);
 79                     rubbishMesh.material.dispose();
 80                     rubbishMesh.geometry.dispose();
 81                     // rubbishMesh.dispose();
 82                     rubbishMesh = null;
 83                 }
 84                 rubbish = null;
 85             }
 86 
 87             // if (mesh.scale.z <=0 ){
 88             //     this.animateFlag = true;
 89             // }
 90 
 91             // if(mesh.scale.z >=1 ) {
 92             //     this.animateFlag = false;
 93             // } 
 94 
 95             // if(this.animateFlag) {
 96             //     mesh.scale.z += 0.01;
 97             // } else {
 98             //     mesh.scale.z -= 0.01;
 99             // }
100         }
101     }

 

 

ExternalRendererLayer:

  1 import * as THREE from 'three'
  2 import Stats from 'three/examples/jsm/libs/stats.module.js'
  3 import * as webMercatorUtils from "@arcgis/core/geometry/support/webMercatorUtils"
  4 import * as externalRenderers from "@arcgis/core/views/3d/externalRenderers"
  5 
  6 export default class ExternalRendererLayer {
  7     constructor({
  8         view,
  9         options
 10     }) {
 11         this.view = view
 12         this.options = options
 13 
 14         this.objects = []
 15         this.scene = null
 16         this.camera = null
 17         this.renderer = null
 18         
 19         this.setup();
 20     }
 21     
 22     setup() {
 23         if (process.env.NODE_ENV !== "production") {
 24             const sid = setTimeout(() => {
 25                 clearTimeout(sid)
 26                 //構建影格率檢視器
 27                 let stats = new Stats()
 28                 stats.setMode(0)
 29                 stats.domElement.style.position = 'absolute'
 30                 stats.domElement.style.left = '0px'
 31                 stats.domElement.style.top = '0px'
 32                 document.body.appendChild(stats.domElement)
 33                 function render() {
 34                   stats.update()
 35                   requestAnimationFrame(render)
 36                 }
 37                 render()
 38             }, 5000)
 39         }
 40     }
 41 
 42     apply() {
 43         let myExternalRenderer = {
 44             setup: context => {
 45                 this.createSetup(context)
 46             },
 47             render: context => {
 48                 this.createRender(context)
 49             }
 50         }
 51         
 52         externalRenderers.add(this.view, myExternalRenderer);
 53     }
 54 
 55     createSetup(context) {
 56         this.scene = new THREE.Scene(); // 場景
 57         this.camera = new THREE.PerspectiveCamera(); // 相機
 58 
 59         this.setLight();
 60 
 61         // 新增座標軸輔助工具
 62         const axesHelper = new THREE.AxesHelper(10000000);
 63         this.scene.Helpers = axesHelper;
 64         this.scene.add(axesHelper);
 65 
 66         this.renderer = new THREE.WebGLRenderer({
 67             context: context.gl, // 可用於將渲染器附加到已有的渲染環境(RenderingContext)中
 68             premultipliedAlpha: false, // renderer是否假設顏色有 premultiplied alpha. 預設為true
 69             // antialias: true
 70             // logarithmicDepthBuffer: false
 71             // logarithmicDepthBuffer: true 
 72         });
 73         this.renderer.setPixelRatio(window.devicePixelRatio); // 設定裝置畫素比。通常用於避免HiDPI裝置上繪圖模糊
 74         this.renderer.setViewport(0, 0, this.view.width, this.view.height); // 視口大小設定
 75         
 76         // 防止Three.js清除ArcGIS JS API提供的緩衝區。
 77         this.renderer.autoClearDepth = false; // 定義renderer是否清除深度快取
 78         this.renderer.autoClearStencil = false; // 定義renderer是否清除模板快取
 79         this.renderer.autoClearColor = false; // 定義renderer是否清除顏色快取
 80         // this.renderer.autoClear = false;
 81         
 82         // ArcGIS JS API渲染自定義離屏緩衝區,而不是預設的幀緩衝區。
 83         // 我們必須將這段程式碼注入到three.js執行時中,以便繫結這些緩衝區而不是預設的緩衝區。
 84         const originalSetRenderTarget = this.renderer.setRenderTarget.bind(
 85             this.renderer
 86         );
 87         this.renderer.setRenderTarget = target => {
 88             originalSetRenderTarget(target);
 89             if (target == null) {
 90                 // 繫結外部渲染器應該渲染到的顏色和深度緩衝區
 91                 context.bindRenderTarget();
 92             }
 93         };
 94         
 95         this.addModels(context);
 96 
 97         context.resetWebGLState();
 98     }
 99 
100     createRender(context) {
101         const cam = context.camera;
102         this.camera.position.set(cam.eye[0], cam.eye[1], cam.eye[2]);
103         this.camera.up.set(cam.up[0], cam.up[1], cam.up[2]);
104         this.camera.lookAt(
105             new THREE.Vector3(cam.center[0], cam.center[1], cam.center[2])
106         );
107         // this.camera.near = 1;
108         // this.camera.far = 100;
109 
110         // 投影矩陣可以直接複製
111         this.camera.projectionMatrix.fromArray(cam.projectionMatrix);
112         
113         this.updateModels(context);
114 
115         this.renderer.state.reset();
116 
117         context.bindRenderTarget();
118 
119         this.renderer.render(this.scene, this.camera);
120 
121         // 請求重繪檢視。
122         externalRenderers.requestRender(this.view);
123 
124         // cleanup
125         context.resetWebGLState();
126     }
127     
128     //經緯度座標轉成三維空間座標
129     lngLatToXY(view, points) {
130     
131         let vector3List; // 頂點陣列
132     
133         let pointXYs;
134     
135     
136         // 計算頂點
137         let transform = new THREE.Matrix4(); // 變換矩陣
138         let transformation = new Array(16);
139     
140         // 將經緯度座標轉換為xy值\
141         let pointXY = webMercatorUtils.lngLatToXY(points[0], points[1]);
142     
143         // 先轉換高度為0的點
144         transform.fromArray(
145             externalRenderers.renderCoordinateTransformAt(
146                 view,
147                 [pointXY[0], pointXY[1], points[
148                     2]], // 座標在地面上的點[x值, y值, 高度值]
149                 view.spatialReference,
150                 transformation
151             )
152         );
153     
154         pointXYs = pointXY;
155     
156         vector3List =
157             new THREE.Vector3(
158                 transform.elements[12],
159                 transform.elements[13],
160                 transform.elements[14]
161             )
162     
163         return {
164             vector3List: vector3List,
165             pointXYs: pointXYs
166         };
167     }
168     
169     setLight() {
170         console.log('setLight')
171         let ambient = new THREE.AmbientLight(0xffffff, 0.7);
172         this.scene.add(ambient);
173         let directionalLight = new THREE.DirectionalLight(0xffffff, 0.7);
174         directionalLight.position.set(100, 300, 200);
175         this.scene.add(directionalLight);
176     }
177     
178     addModels(context) {
179         console.log('addModels')
180     }
181     
182     updateModels(context) {
183         // console.log('updateModels')
184     }
185     
186 }
View Code

FlashLayer:

  1 import mapx from '@/utils/mapUtils.js';
  2 import Polygon from "@arcgis/core/geometry/Polygon";
  3 import Point from "@arcgis/core/geometry/Point";
  4 import * as THREE from 'three'
  5 import ExternalRendererLayer from './ExternalRendererLayer.js'
  6 import Graphic from "@arcgis/core/Graphic";
  7 import SpatialReference from '@arcgis/core/geometry/SpatialReference'
  8 import * as externalRenderers from "@arcgis/core/views/3d/externalRenderers"
  9 
 10 const COLOR_RING = 0xff0000;
 11 const FLASH_TIMES = 2;
 12 
 13 export default class FlashLayer extends ExternalRendererLayer {
 14     constructor({
 15         view,
 16         options
 17     }) {
 18         super({
 19             view,
 20             options
 21         })
 22     }
 23 
 24     setup() {
 25         this.group = new THREE.Group();
 26         // this.animateFlag = false;
 27     }
 28 
 29     addModels(context) {
 30         super.addModels(context);
 31         
 32         this.scene.add(this.group);
 33         this.objects.push(this.group);
 34     }
 35 
 36     excute(point, height) {
 37         // let pointList = [
 38         //     [114.31456780904838, 30.55355011036358, 0],
 39         //     [114.31456780904838, 30.55355011036358, 2000]
 40         // ];
 41         let pointList = [
 42             [
 43                 point.longitude,
 44                 point.latitude,
 45                 0
 46             ],
 47             [
 48                 point.longitude,
 49                 point.latitude,
 50                 height
 51             ]
 52         ]
 53 
 54         let linePoints = [];
 55         //確定幾何體位置
 56         pointList.forEach((item) => {
 57             var renderLinePoints = this.lngLatToXY(this.view, [item[0], item[1], item[2]]);
 58             linePoints.push(new THREE.Vector3(renderLinePoints.vector3List.x, renderLinePoints
 59                 .vector3List.y, renderLinePoints.vector3List.z));
 60         })
 61 
 62         const lineImg = require('../../../../public/static/img/line.png')
 63         let lineTexture = new THREE.TextureLoader().load(lineImg)
 64         lineTexture.wrapS = lineTexture.wrapT = THREE.RepeatWrapping; //每個都重複
 65         lineTexture.repeat.set(1, 1)
 66         lineTexture.needsUpdate = true
 67 
 68         let lineMaterial = new THREE.MeshBasicMaterial({
 69             map: lineTexture,
 70             side: THREE.DoubleSide,
 71             transparent: true,
 72             opacity: 1
 73         })
 74 
 75         // CatmullRomCurve3建立一條平滑的三維樣條曲線
 76         let curve = new THREE.CatmullRomCurve3(linePoints) // 曲線路徑
 77 
 78         // 建立管道
 79         let tubeGeometry = new THREE.TubeGeometry(curve, 1, 16)
 80 
 81         let mesh = new THREE.Mesh(tubeGeometry, lineMaterial);
 82         mesh.name = 'FlashLayer_Line';
 83         mesh.scale.z = 0;
 84         mesh.layers.set(0);
 85         this.group.add(mesh);
 86 
 87         //光圈
 88         const ringUserData = {
 89             width: 300,
 90             innerRadius: 0,
 91             opacity: 1,
 92             opacityFlag: false,
 93             per: 10,
 94             innerMax: 800,
 95             times: 0,
 96             line: mesh
 97         };
 98         const ringGeometry = new THREE.RingGeometry(ringUserData.innerRadius, ringUserData.innerRadius +
 99             ringUserData.width, 32);
100         const ringMaterial = new THREE.MeshPhongMaterial({
101             color: COLOR_RING,
102             emissive: COLOR_RING,
103             side: THREE.DoubleSide,
104             flatShading: true,
105             wireframe: false,
106             transparent: true,
107             opacity: ringUserData.opacity
108         });
109         const ringMesh = new THREE.Mesh(ringGeometry, ringMaterial);
110         const ringPoint = linePoints[1];
111         console.log(ringPoint);
112         ringMesh.position.set(ringPoint.x, ringPoint.y, ringPoint.z / 2 + (Math.random() * ringPoint.z / 4 | 0) );
113         ringMesh.name = 'FlashLayer_Ring';
114         ringMesh.userData = ringUserData;
115         ringMesh.layers.set(0);
116 
117         this.group.add(ringMesh);
118     }
119 
120     updateModels(context) {
121         super.updateModels(context)
122 
123         if (this.group.children.length) {
124             let rubbish = [];
125             for (let i = this.group.children.length - 1; i >= 0; i--) {
126                 const mesh = this.group.children[i];
127                 if (mesh.name === 'FlashLayer_Line') {
128                     mesh.material.map.offset.x -= 0.02;
129                     mesh.scale.z += 0.03;
130                     if(mesh.scale.z >= 1) {
131                         mesh.scale.z = 1;
132                     }
133                     continue;
134                 }
135                 if (mesh.name === 'FlashLayer_Ring') {
136                     const ringUserData = mesh.userData;
137                     const per = ringUserData.per;
138                     ringUserData.innerRadius += per;
139                     ringUserData.opacity -= 1 / (ringUserData.innerMax / per);
140 
141                     if (ringUserData.innerRadius > ringUserData.innerMax) {
142                         ringUserData.innerRadius = 0;
143                         ringUserData.opacity = 1;
144                         ringUserData.times++;
145                         
146                         if(ringUserData.times === FLASH_TIMES) {
147                             rubbish.push(mesh)
148                             continue;
149                         }
150                     }
151 
152                     // if(ringUserData.opacityFlag) {
153                     //     ringUserData.opacity += 1 / 800 / 5;
154                     // } else {
155                     //     ringUserData.opacity -= 1 / 800 / 5;
156                     // }
157 
158                     // if(ringUserData.opacity >= 1) {
159                     //     ringUserData.opacity = 1;
160                     //     ringUserData.opacityFlag = false;
161                     // }
162                     // if(ringUserData.opacity <= 0) {
163                     //     ringUserData.opacity = 0;
164                     //     ringUserData.opacityFlag = true;
165                     // }
166 
167                     const ringGeometry = new THREE.RingGeometry(ringUserData.innerRadius, ringUserData.innerRadius +
168                         ringUserData.width, 32);
169                     const ringMaterial = new THREE.MeshPhongMaterial({
170                         color: COLOR_RING,
171                         emissive: COLOR_RING,
172                         side: THREE.DoubleSide,
173                         flatShading: true,
174                         wireframe: false,
175                         transparent: true,
176                         opacity: ringUserData.opacity
177                     });
178                     mesh.geometry.dispose();
179                     mesh.material.dispose();
180                     mesh.geometry = ringGeometry;
181                     mesh.material = ringMaterial;
182                     mesh.rotation.z += 1;
183 
184                     continue;
185                 }
186             }
187             
188             if(rubbish.length) {
189                 for(let i = 0; i < rubbish.length; i++) {
190                     let rubbishMesh = rubbish[i];
191                     let line = rubbishMesh.userData.line;
192                     this.group.remove(line);
193                     line.material.dispose();
194                     line.geometry.dispose();
195                     // line.dispose();
196                     line = null;
197                     this.group.remove(rubbishMesh);
198                     rubbishMesh.material.dispose();
199                     rubbishMesh.geometry.dispose();
200                     // rubbishMesh.dispose();
201                     rubbishMesh = null;
202                 }
203                 rubbish = null;
204             }
205 
206             // if (mesh.scale.z <=0 ){
207             //     this.animateFlag = true;
208             // }
209 
210             // if(mesh.scale.z >=1 ) {
211             //     this.animateFlag = false;
212             // } 
213 
214             // if(this.animateFlag) {
215             //     mesh.scale.z += 0.01;
216             // } else {
217             //     mesh.scale.z -= 0.01;
218             // }
219         }
220     }
221 
222     transparentObject(geometry, material) {
223         var obj = new THREE.Object3D();
224         var mesh = new THREE.Mesh(geometry, material);
225         mesh.material.side = THREE.BackSide; // back faces
226         mesh.renderOrder = 0;
227         obj.add(mesh);
228 
229         var mesh = new THREE.Mesh(geometry, material.clone());
230         mesh.material.side = THREE.FrontSide; // front faces
231         mesh.renderOrder = 1;
232         obj.add(mesh);
233         return obj
234     }
235 
236 }
View Code

呼叫案例:點選地圖顯示光圈閃爍,MapBuilder是我封裝的載入底圖的類,各位大佬自己換掉,隨便加個底圖圖層

 1 <template>
 2     <div class="root"><div id="map" ref="rootmap"></div></div>
 3 </template>
 4 
 5 <script>
 6 import MapBuilder from './core/MapBuilder.js';
 7 import FlashLayer from './core/FlashLayer.js';
 8 
 9 export default {
10     name: 'base-map',
11     data() {
12         return {
13             map: null
14         };
15     },
16     computed: {},
17     created() {
18         this.inited = false;
19         this.view = null;
20     },
21     mounted() {
22         this.setup();
23     },
24     methods: {
25         setup() {
26             let mb = new MapBuilder({
27                 id: 'map',
28                 complete: (m, v, b) => {
29                     this.map = m;
30                     this.inited = true;
31                     this.view = v;
32                     
33                     v.on('click', event => {
34                         console.log(event);
35                         this.flashLayer.excute(event.mapPoint, 2000);
36                     });
37                 }
38             });
39         }
40     }
41 };
42 </script>
43 
44 <style lang="scss" scoped>
45 .root {
46     position: absolute;
47     width: 100%;
48     height: 100%;
49     #map {
50         width: 100%;
51         height: 100%;
52         outline: none;
53         // background-color: $color-grey;
54         // background-color: black;
55     }
56 }
57 
58 ::v-deep {
59     .esri-ui-top-left {
60         left: 410px;
61         top: 40px;
62     }
63     h2.esri-widget__heading {
64         font-size: 12px;
65     }
66     .esri-view-width-xlarge .esri-popup__main-container {
67         width: 300px;
68     }
69     .esri-view .esri-view-surface--inset-outline:focus::after {
70         outline: auto 0px Highlight !important;
71         outline: auto 0px -webkit-focus-ring-color !important;
72     }
73 }
74 </style>
View Code