数据可视化【原创】vue+arcgis+threejs 实现流光立体墙效果

发布时间 2023-08-31 15:16:06作者: Binyy_Wuhan

本文适合对vue,arcgis4.x,threejs,ES6较熟悉的人群食用。

效果图:

 素材:

 

主要思路:

先用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