数据可视化【原创】vue+arcgis+threejs 实现流光立体墙效果
阅读原文时间:2023年08月31日阅读:2

本文适合对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 }

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 };