• 数据可视化【原创】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 }
    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 
     4 
     5 
    41 
    42 
    View Code

     

  • 相关阅读:
    设计模式——行为型设计模式
    【C++ 科学计算】dlib 机器学习和数据分析编译安装
    为什么yolov8val的dfl_loss一直上升呢
    springboot实战(五)之sql&业务日志输出,重要
    大数据flink篇之一-基础知识
    第九章、类的生命周期
    数据挖掘:用ID3算法或者朴素贝叶斯分析一个数据集
    单元测试 CI/CD
    Maven中央仓库地址配置大全
    数字孪生论文阅读笔记【1】
  • 原文地址:https://www.cnblogs.com/loveFlex/p/17669620.html