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

2023-08-31 18:03:10

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

效果圖:

 素材:(有2個小圖片哦,第二張是白色的乍一看看不見!)

 

主要思路:

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

子類AreaLayer繼承它,並在裡面實現繪製流光邊界牆的方法,這裡用的BufferGeometry構建幾何物件,材質是ShaderMaterial著色器。關鍵點就在於下面這2個方法。

1:建立材質ShaderMaterial createWallMaterial

 1 /**
 2  * 建立流體牆體材質
 3  * option =>
 4  * params bgUrl flowUrl
 5  * **/
 6 const createWallMaterial = ({
 7     bgTexture,
 8     flowTexture
 9 }) => {
10     // 頂點著色器
11     const vertexShader = `
12             varying vec2 vUv;
13             varying vec3 fNormal;
14             varying vec3 vPosition;
15             void main(){
16                     vUv = uv;
17                     vPosition = position;
18                     vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
19                     gl_Position = projectionMatrix * mvPosition;
20             }
21         `;
22     // 片元著色器
23     const fragmentShader = `
24             uniform float time;
25             varying vec2 vUv;
26             uniform sampler2D flowTexture;
27             uniform sampler2D bgTexture;
28             void main( void ) {
29                 vec2 position = vUv;
30                 vec4 colora = texture2D( flowTexture, vec2( vUv.x, fract(vUv.y - time )));
31                 vec4 colorb = texture2D( bgTexture , position.xy);
32                 gl_FragColor = colorb + colorb * colora;
33             }
34         `;
35     // 允許平鋪
36     flowTexture.wrapS = THREE.RepeatWrapping;
37     return new THREE.ShaderMaterial({
38         uniforms: {
39             time: {
40                 value: 0,
41             },
42             flowTexture: {
43                 value: flowTexture,
44             },
45             bgTexture: {
46                 value: bgTexture,
47             },
48         },
49         transparent: true,
50         depthWrite: false,
51         depthTest: false,
52         side: THREE.DoubleSide,
53         vertexShader: vertexShader,
54         fragmentShader: fragmentShader,
55     });
56 };

2:建立BufferGeometry createWallByPath

 1 /**
 2  * 通過path構建牆體
 3  * option =>
 4  * params height path material expand(是否需要擴充套件路徑)
 5  * **/
 6 export const createWallByPath = ({
 7     height = 10,
 8     path = [],
 9     material,
10     expand = true,
11 }) => {
12     let verticesByTwo = null;
13     // 1.處理路徑資料  每兩個頂點為為一組
14     if (expand) {
15         // 1.1向y方向拉伸頂點
16         verticesByTwo = path.reduce((arr, [x, y, z]) => {
17             return arr.concat([
18                 [
19                     [x, y, z],
20                     [x, y, z + height],
21                 ],
22             ]);
23         }, []);
24     } else {
25         // 1.2 已經處理好路徑資料
26         verticesByTwo = path;
27     }
28     // 2.解析需要渲染的四邊形 每4個頂點為一組
29     const verticesByFour = verticesByTwo.reduce((arr, item, i) => {
30         if (i === verticesByTwo.length - 1) return arr;
31         return arr.concat([
32             [item, verticesByTwo[i + 1]]
33         ]);
34     }, []);
35     // 3.將四邊形面轉換為需要渲染的三頂點面
36     const verticesByThree = verticesByFour.reduce((arr, item) => {
37         const [
38             [point1, point2],
39             [point3, point4]
40         ] = item;
41         return arr.concat(
42             ...point2,
43             ...point1,
44             ...point4,
45             ...point1,
46             ...point3,
47             ...point4
48         );
49     }, []);
50     const geometry = new THREE.BufferGeometry();
51     // 4. 設定position
52     const vertices = new Float32Array(verticesByThree);
53     geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
54     // 5. 設定uv 6個點為一個週期 [0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1]
55 
56     // 5.1 以18個頂點為單位分組
57     const pointsGroupBy18 = new Array(verticesByThree.length / 3 / 6)
58         .fill(0)
59         .map((item, i) => {
60             return verticesByThree.slice(i * 3 * 6, (i + 1) * 3 * 6);
61         });
62     // 5.2 按uv週期分組
63     const pointsGroupBy63 = pointsGroupBy18.map((item, i) => {
64         return new Array(item.length / 3)
65             .fill(0)
66             .map((it, i) => item.slice(i * 3, (i + 1) * 3));
67     });
68     // 5.3根據BoundingBox確定uv平鋪範圍
69     geometry.computeBoundingBox();
70     const {
71         min,
72         max
73     } = geometry.boundingBox;
74     const rangeX = max.x - min.x;
75     const uvs = [].concat(
76         ...pointsGroupBy63.map((item) => {
77             const point0 = item[0];
78             const point5 = item[5];
79             const distance =
80                 new THREE.Vector3(...point0).distanceTo(new THREE.Vector3(...point5)) /
81                 (rangeX / 10);
82             return [0, 1, 0, 0, distance, 1, 0, 0, distance, 0, distance, 1];
83         })
84     );
85     geometry.setAttribute(
86         "uv",
87         new THREE.BufferAttribute(new Float32Array(uvs), 2)
88     );
89     const meshMat =
90         material ||
91         new THREE.MeshBasicMaterial({
92             color: 0x00ffff,
93             side: THREE.DoubleSide,
94         });
95     return new THREE.Mesh(geometry, meshMat);
96 };

3:最後再updateModels裡面更新貼圖的位置(其實就是render事件)

1 updateModels(context) {
2         super.updateModels(context);
3         
4         this.objects.forEach(obj => {
5             obj.material.uniforms.time.value += 0.01;
6         })
7     }

 

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

AreaLayer:原始碼中mapx.queryTask是封裝了arcgis的query查詢,這個可以替換掉,我只是要接收返回的rings陣列,自行構建靜態資料也行

  1 import mapx from '@/utils/mapUtils.js';
  2 import * as THREE from 'three'
  3 import ExternalRendererLayer from './ExternalRendererLayer.js'
  4 import Graphic from "@arcgis/core/Graphic";
  5 import SpatialReference from '@arcgis/core/geometry/SpatialReference'
  6 import * as externalRenderers from "@arcgis/core/views/3d/externalRenderers"
  7 
  8 const WALL_HEIGHT = 200;
  9 
 10 export default class ArealLayer extends ExternalRendererLayer {
 11     constructor({
 12         view,
 13         options
 14     }) {
 15         super({
 16             view,
 17             options
 18         })
 19     }
 20 
 21     addModels(context) {
 22         super.addModels(context);
 23         // let pointList = [
 24         //     [114.31456780904838, 30.55355011036358],
 25         //     [114.30888002358996, 30.553227103422344],
 26         //     [114.31056780904838, 30.56355011036358],
 27         //     [114.31256780904838, 30.58355011036358]
 28         // ];
 29         
 30         const url = config.mapservice[1].base_url + config.mapservice[1].jd_url;
 31         // const url = 'http://10.100.0.132:6080/arcgis/rest/services/wuchang_gim/gim_region/MapServer/2';
 32         mapx.queryTask(url, {
 33             where: '1=1',
 34             returnGeometry: true
 35         }).then(featureSet => {
 36             if (featureSet.length > 0) {
 37                 featureSet.forEach(feature => {
 38                     const polygon = feature.geometry;
 39                     const rings = polygon.rings;
 40                     rings.forEach(ring => {
 41                         this._addModel(ring);
 42                     })
 43                 })
 44             }
 45         }).catch(error => {
 46             console.log(error)
 47         })
 48     }
 49 
 50     updateModels(context) {
 51         super.updateModels(context);
 52         
 53         this.objects.forEach(obj => {
 54             obj.material.uniforms.time.value += 0.01;
 55         })
 56     }
 57     
 58     _addModel(pointList) {
 59         // =====================mesh載入=================================//
 60          let linePoints = [];
 61         
 62         //確定幾何體位置
 63         pointList.forEach((item) => {
 64             var renderLinePoints = this.lngLatToXY(this.view, [item[0], item[1], 0]);
 65             linePoints.push(new THREE.Vector3(renderLinePoints.vector3List.x, renderLinePoints
 66                 .vector3List.y, renderLinePoints.vector3List.z));
 67         })
 68         
 69         // "https://model.3dmomoda.com/models/47007127aaf1489fb54fa816a15551cd/0/gltf/116802027AC38C3EFC940622BC1632BA.jpg"
 70         const bgImg = require('../../../../public/static/img/b9a06c0329c3b4366b972632c94e1e8.png');
 71         const bgTexture = new THREE.TextureLoader().load(bgImg);
 72         const flowImg = require('../../../../public/static/img/F3E2E977BDB335778301D9A1FA4A4415.png');
 73         const flowTexture = new THREE.TextureLoader().load(flowImg);
 74         const material = createWallMaterial({
 75             bgTexture,
 76             flowTexture
 77         });
 78         const wallMesh = createWallByPath({
 79             height: WALL_HEIGHT,
 80             path: linePoints,
 81             material,
 82             expand: true
 83         });
 84         this.scene.add(wallMesh);
 85         this.objects.push(wallMesh);
 86     }
 87 }
 88 
 89 /**
 90  * 建立流體牆體材質
 91  * option =>
 92  * params bgUrl flowUrl
 93  * **/
 94 const createWallMaterial = ({
 95     bgTexture,
 96     flowTexture
 97 }) => {
 98     // 頂點著色器
 99     const vertexShader = `
100             varying vec2 vUv;
101             varying vec3 fNormal;
102             varying vec3 vPosition;
103             void main(){
104                     vUv = uv;
105                     vPosition = position;
106                     vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
107                     gl_Position = projectionMatrix * mvPosition;
108             }
109         `;
110     // 片元著色器
111     const fragmentShader = `
112             uniform float time;
113             varying vec2 vUv;
114             uniform sampler2D flowTexture;
115             uniform sampler2D bgTexture;
116             void main( void ) {
117                 vec2 position = vUv;
118                 vec4 colora = texture2D( flowTexture, vec2( vUv.x, fract(vUv.y - time )));
119                 vec4 colorb = texture2D( bgTexture , position.xy);
120                 gl_FragColor = colorb + colorb * colora;
121             }
122         `;
123     // 允許平鋪
124     flowTexture.wrapS = THREE.RepeatWrapping;
125     return new THREE.ShaderMaterial({
126         uniforms: {
127             time: {
128                 value: 0,
129             },
130             flowTexture: {
131                 value: flowTexture,
132             },
133             bgTexture: {
134                 value: bgTexture,
135             },
136         },
137         transparent: true,
138         depthWrite: false,
139         depthTest: false,
140         side: THREE.DoubleSide,
141         vertexShader: vertexShader,
142         fragmentShader: fragmentShader,
143     });
144 };
145 
146 
147 /**
148  * 通過path構建牆體
149  * option =>
150  * params height path material expand(是否需要擴充套件路徑)
151  * **/
152 export const createWallByPath = ({
153     height = 10,
154     path = [],
155     material,
156     expand = true,
157 }) => {
158     let verticesByTwo = null;
159     // 1.處理路徑資料  每兩個頂點為為一組
160     if (expand) {
161         // 1.1向y方向拉伸頂點
162         verticesByTwo = path.reduce((arr, [x, y, z]) => {
163             return arr.concat([
164                 [
165                     [x, y, z],
166                     [x, y, z + height],
167                 ],
168             ]);
169         }, []);
170     } else {
171         // 1.2 已經處理好路徑資料
172         verticesByTwo = path;
173     }
174     // 2.解析需要渲染的四邊形 每4個頂點為一組
175     const verticesByFour = verticesByTwo.reduce((arr, item, i) => {
176         if (i === verticesByTwo.length - 1) return arr;
177         return arr.concat([
178             [item, verticesByTwo[i + 1]]
179         ]);
180     }, []);
181     // 3.將四邊形面轉換為需要渲染的三頂點面
182     const verticesByThree = verticesByFour.reduce((arr, item) => {
183         const [
184             [point1, point2],
185             [point3, point4]
186         ] = item;
187         return arr.concat(
188             ...point2,
189             ...point1,
190             ...point4,
191             ...point1,
192             ...point3,
193             ...point4
194         );
195     }, []);
196     const geometry = new THREE.BufferGeometry();
197     // 4. 設定position
198     const vertices = new Float32Array(verticesByThree);
199     geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
200     // 5. 設定uv 6個點為一個週期 [0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1]
201 
202     // 5.1 以18個頂點為單位分組
203     const pointsGroupBy18 = new Array(verticesByThree.length / 3 / 6)
204         .fill(0)
205         .map((item, i) => {
206             return verticesByThree.slice(i * 3 * 6, (i + 1) * 3 * 6);
207         });
208     // 5.2 按uv週期分組
209     const pointsGroupBy63 = pointsGroupBy18.map((item, i) => {
210         return new Array(item.length / 3)
211             .fill(0)
212             .map((it, i) => item.slice(i * 3, (i + 1) * 3));
213     });
214     // 5.3根據BoundingBox確定uv平鋪範圍
215     geometry.computeBoundingBox();
216     const {
217         min,
218         max
219     } = geometry.boundingBox;
220     const rangeX = max.x - min.x;
221     const uvs = [].concat(
222         ...pointsGroupBy63.map((item) => {
223             const point0 = item[0];
224             const point5 = item[5];
225             const distance =
226                 new THREE.Vector3(...point0).distanceTo(new THREE.Vector3(...point5)) /
227                 (rangeX / 10);
228             return [0, 1, 0, 0, distance, 1, 0, 0, distance, 0, distance, 1];
229         })
230     );
231     geometry.setAttribute(
232         "uv",
233         new THREE.BufferAttribute(new Float32Array(uvs), 2)
234     );
235     const meshMat =
236         material ||
237         new THREE.MeshBasicMaterial({
238             color: 0x00ffff,
239             side: THREE.DoubleSide,
240         });
241     return new THREE.Mesh(geometry, meshMat);
242 };
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 AreaLayer from './core/AreaLayer-flow-tube2.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                     this.areaLayer = new AreaLayer({ view: v });
34                     this.areaLayer.apply();
35                 }
36             });
37         }
38     }
39 };
40 </script>
41 
42 <style lang="scss" scoped>
43 .root {
44     position: absolute;
45     width: 100%;
46     height: 100%;
47     #map {
48         width: 100%;
49         height: 100%;
50         outline: none;
51         // background-color: $color-grey;
52         // background-color: black;
53     }
54 }
55 
56 ::v-deep {
57     .esri-ui-top-left {
58         left: 410px;
59         top: 40px;
60     }
61     h2.esri-widget__heading {
62         font-size: 12px;
63     }
64     .esri-view-width-xlarge .esri-popup__main-container {
65         width: 300px;
66     }
67     .esri-view .esri-view-surface--inset-outline:focus::after {
68         outline: auto 0px Highlight !important;
69         outline: auto 0px -webkit-focus-ring-color !important;
70     }
71 }
72 </style>
View Code