数据可视化【原创】vue+arcgis+threejs 实现流光立体墙效果
本文适合对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 }
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 };
调用案例: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>
作者: Binyy
出处: https://www.cnblogs.com/loveFlex
城市:wuhan
微信:momakeyy
详细源码请移步,记得点个星星噢~ https://gitee.com/binyylovesino/lilo-ui 欢迎各路大佬指导、提问~
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出 原文链接 如有问题, 可邮件(408460486@qq.com)或者微信咨询.
posted on 2023-08-31 15:11 Binyy_Wuhan 阅读(1818) 评论(1) 编辑 收藏 举报