• 数据可视化【原创】vue+arcgis+threejs 实现海量建筑物房屋渲染,性能优化


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

    效果图:

     

     

    先报备一下版本号

     "vue": "^2.6.11"

    "@arcgis/core": "^4.21.2"

    "three": "^0.149.0"

    语法:vue,ES6

     

    其实现在主流很多海量建筑渲染的方案是加载3DTiles服务图层,可是奈何我们这里没有这个配套。只能全部依靠前端来渲染,目前数据量在6万级别的不规则建筑物房屋。

    试过很多方案,当然,还有一个很重要的因素,就是你的机器显卡厉不厉害,反正我的很垃圾,GTX1050,笔记本,我把chrome的强制使用显卡渲染开启了,避免集成显卡出来搞笑。以下方案中的代码是基于项目接口的,不能直接跑起来,但是关键的策略逻辑已经完全体现。

     

    先说结论,我选的方案3。

    1:首先根据视口内切圆的范围来查询,把构建了的要素缓存,在地图漫游的时候在缓存中查找,避免重复构建,移出视口内切圆范围的要素移除(缓存不清除),其实就和瓦片加载机制(行列号级别缓存)类似。还要限制级别,如果当级别很小的时候,视口内切圆中的数据量太多,会卡顿,所以这种方案最好是做达到一定级别,房屋图层渐变显示,反之渐变消失,代码中有。再用arcgis的graphicslayer,计算faces,构建Mesh对象,这个构建Mesh的过程需要根据3D知识自己写(getFaces,getTopFaces,getSideFaces),代码中有。这个方案性能很一般,大概只能大几千的数据量,顶多1万,并且在数据量支持不了的时候我开启了延迟队列加载的策略。样式控制相对于featurelayer灵活一点,效果也可控制,比如使用gsap动画库做伸展效果。

      1 import GraphicsLayer from "@arcgis/core/layers/GraphicsLayer";
      2 import Graphic from "@arcgis/core/Graphic";
      3 import Mesh from "@arcgis/core/geometry/Mesh";
      4 import Polygon from "@arcgis/core/geometry/Polygon";
      5 import Polyline from "@arcgis/core/geometry/Polyline";
      6 import Circle from "@arcgis/core/geometry/Circle";
      7 import * as watchUtils from "@arcgis/core/core/watchUtils";
      8 import * as geometryEngine from "@arcgis/core/geometry/geometryEngine";
      9 import mapx from '@/utils/mapUtils.js';
     10 
     11 export default class BMLayer {
     12     constructor(ops) {
     13         this.$url = ops.url;
     14         this.$view = ops.view;
     15         // this.$zoom = ops.zoom;
     16         this.$ofomatter = ops.ofomatter;
     17         this.$idkey = ops.idkey;
     18         this.$click = ops.click;
     19         this.$zfomatter = ops.zfomatter;
     20         this.$wfomatter = ops.wfomatter;
     21         this.$sfomatter = ops.sfomatter;
     22 
     23         this.setup()
     24     }
     25 
     26     setup() {
     27         this.layer = null;
     28         this.highlightSelect = null;
     29         this.preModels = {};
     30         this.ef = 1.2;
     31         this.currentCircle = null;
     32         this.autoLoad = false;
     33 
     34         this.rendering = false;
     35 
     36         this.layer = new GraphicsLayer();
     37         this.$view.map.add(this.layer);
     38 
     39         this.extentChanged();
     40     }
     41 
     42     addModel(fs) {
     43         for (let key in this.preModels) {
     44             var m = this.preModels[key];
     45             if (!this.$view.extent.intersects(m.geometry)) {
     46                 this.layer.remove(m);
     47                 delete this.preModels[key];
     48             }
     49         }
     50         var per = 100;
     51         if (fs.length < per) {
     52             per = fs.length;
     53         }
     54         var sid = setInterval(() => {
     55             var i = 0;
     56             for (; i < per; i++) {
     57                 var f = fs.pop();
     58                 if (f) {
     59                     var att = f.attributes;
     60                     var uid = att[this.$idkey];
     61 
     62                     if (this.preModels.hasOwnProperty(uid)) {
     63 
     64                     } else {
     65                         var z = this.$zfomatter(att);
     66                         var model = this.createModel(f, z, this.getBaseSymbol());
     67                         this.layer.add(model);
     68 
     69                         this.preModels[uid] = model;
     70                     }
     71                 } else {
     72                     this.rendering = false;
     73                     clearInterval(sid);
     74                     break;
     75                 }
     76             }
     77         }, 25);
     78     }
     79 
     80     click(results, mapPoint) {
     81         if (results && results.length > 0) {
     82             var grah = results[0].graphic;
     83             if (grah.layer === this.layer) {
     84                 if (this.highlightSelect) {
     85                     this.highlightSelect.remove();
     86                 }
     87 
     88                 this.$view.whenLayerView(grah.layer).then(
     89                     layerView => {
     90                         this.highlightSelect = layerView.highlight(grah);
     91                     });
     92                 this.$click(grah, mapPoint, this.$view);
     93             } else {
     94                 if (this.highlightSelect) {
     95                     this.highlightSelect.remove();
     96                 }
     97                 // this.$view.popup.close()
     98             }
     99         } else {
    100             if (this.highlightSelect) {
    101                 this.highlightSelect.remove();
    102             }
    103             // this.$view.popup.close()
    104         }
    105     }
    106 
    107     clearHighlight() {
    108         if (this.highlightSelect) {
    109             this.highlightSelect.remove();
    110         }
    111     }
    112 
    113     setAutoLoad(v) {
    114         this.autoLoad = v
    115     }
    116 
    117     extentChanged() {
    118         return watchUtils.whenTrue(this.$view, "stationary", () => {
    119             // console.log(this.$view.zoom)
    120             const flag = this.$ofomatter(this.$view);
    121             if (flag) {
    122                 if (!this.rendering) {
    123                     this.rendering = true;
    124                     if (this.autoLoad) {
    125                         this.loadData();
    126                     }
    127                 }
    128                 // this.layer.visible = true;
    129                 if (this.layer.opacity === 0) {
    130                     this.fadeVisibilityOn(this.$view, this.layer, true)
    131                 }
    132             } else {
    133                 // this.clearLayer();
    134                 this.rendering = false;
    135                 // this.layer.visible = false;
    136                 if (this.layer.opacity === 1) {
    137                     this.fadeVisibilityOn(this.$view, this.layer, false)
    138                 }
    139             }
    140         });
    141     }
    142 
    143     loadData() {
    144         // var r = this.getRadius(1.5);
    145         // var p = this.$view.center.clone();
    146         // p.z = 1;
    147         // this.currentCircle = new Circle(p, {
    148         //     radius: r
    149         // });
    150         let where = ''
    151         if (this.$wfomatter) {
    152             where = this.$wfomatter();
    153             // console.log(where)
    154         }
    155         mapx.queryTask(this.$url, {
    156             where: where,
    157             outSpatialReference: '4326',
    158             geometry: this.$view.extent,
    159             returnGeometry: true
    160         }).then(featureSet => {
    161             this.addModel(featureSet);
    162         }).catch(error => {})
    163     }
    164 
    165     clearLayer() {
    166         this.layer.removeAll();
    167         this.preModels = {};
    168     }
    169 
    170     createModel(f, h, sym) {
    171         var geo = f.geometry;
    172         var ris = geo.rings[0];
    173         ris.pop();
    174         var len = ris.length;
    175         var pos = new Array((len - 1) * 2 * 3);
    176         var ii = 0;
    177         for (; ii < len; ii++) {
    178             var ary = ris[ii];
    179             pos[ii * 3] = ary[0];
    180             pos[ii * 3 + 1] = ary[1];
    181             pos[ii * 3 + 2] = 0;
    182             pos[ii * 3 + len * 3] = ary[0];
    183             pos[ii * 3 + len * 3 + 1] = ary[1];
    184             pos[ii * 3 + len * 3 + 2] = h;
    185         }
    186 
    187         var polygon = new Polygon({
    188             type: "polygon",
    189             rings: [ris]
    190         });
    191 
    192         var ll = pos.length / 2 / 3;
    193         var faces = this.getFaces(polygon, ll);
    194         var mesh = new Mesh({
    195             vertexAttributes: {
    196                 position: pos
    197             },
    198             components: [{
    199                 faces: faces
    200             }],
    201         });
    202 
    203         let symbol
    204         if (this.$sfomatter) {
    205             symbol = this.getBaseSymbol(this.$sfomatter(f))
    206         } else {
    207             symbol = sym
    208         }
    209         var graphic = new Graphic({
    210             attributes: f.attributes,
    211             geometry: mesh,
    212             symbol: symbol
    213         });
    214 
    215         return graphic;
    216     }
    217 
    218     getFaces(polygon, len) {
    219         var topfaces = this.getTopFaces(polygon);
    220         var sidefaces = this.getSideFaces(len);
    221         //                var i = 0;
    222         //                for(; i < topfaces.length; i++) {
    223         //                    var t = topfaces[i];
    224         //                    sidefaces.push(t);
    225         //                }
    226         var i = 0;
    227         for (; i < topfaces.length; i++) {
    228             var t = topfaces[i];
    229             sidefaces.push(t + len);
    230         }
    231         return sidefaces;
    232     }
    233 
    234     getTopFaces(polygon) {
    235         var temp = Mesh.createFromPolygon(polygon, {});
    236         var faces = temp.components[0].faces;
    237         return faces;
    238     }
    239 
    240     getSideFaces(l) {
    241         var fas = [];
    242         var a = [];
    243         var i = 0;
    244         for (; i < l; i++) {
    245             var n0 = 0;
    246             var n1 = 0;
    247             var n2 = 0;
    248             var n3 = 0;
    249             if (i + 1 == l) {
    250                 n0 = i;
    251                 n1 = 0;
    252                 n2 = i + l;
    253                 n3 = i + 1;
    254             } else {
    255                 n0 = i;
    256                 n1 = i + 1;
    257                 n2 = i + l;
    258                 n3 = i + l + 1;
    259             }
    260             fas.push(n0, n1, n2, n1, n2, n3);
    261         } //console.log(fas);
    262         return fas;
    263     }
    264 
    265     getRadius() {
    266         var extent = this.$view.extent;
    267         var paths = [
    268             [
    269                 [extent.xmin, extent.ymin],
    270                 [extent.xmax, extent.ymax]
    271             ]
    272         ];
    273         var line = new Polyline({
    274             paths: paths,
    275             spatialReference: this.$view.spatialReference
    276         });
    277         var d = geometryEngine.geodesicLength(line, 9001);
    278         return d * 0.5 * this.ef;
    279     }
    280 
    281     getBaseSymbol(color = [224, 224, 224, 0.8]) {
    282         return {
    283             type: "mesh-3d",
    284             symbolLayers: [{
    285                 type: "fill",
    286                 material: {
    287                     color: color,
    288                     colorMixMode: "tint"
    289                 }
    290             }]
    291         }
    292     }
    293 
    294     fadeVisibilityOn(view, layer, flag) {
    295         let animating = true;
    296         let opacity = flag ? 0 : 1;
    297         // fade layer's opacity from 0 to
    298         // whichever value the user has configured
    299         const finalOpacity = flag ? 1 : 0;
    300         layer.opacity = opacity;
    301 
    302         view.whenLayerView(layer).then((layerView) => {
    303             function incrementOpacityByFrame() {
    304                 if (opacity >= finalOpacity && animating) {
    305                     layer.opacity = finalOpacity;
    306                     animating = false;
    307                     return;
    308                 }
    309 
    310                 layer.opacity = opacity;
    311                 opacity += 0.07;
    312 
    313                 requestAnimationFrame(incrementOpacityByFrame);
    314             }
    315 
    316             function decrementOpacityByFrame() {
    317                 if (opacity <= finalOpacity && animating) {
    318                     layer.opacity = finalOpacity;
    319                     animating = false;
    320                     return;
    321                 }
    322 
    323                 layer.opacity = opacity;
    324                 opacity -= 0.07;
    325 
    326                 requestAnimationFrame(decrementOpacityByFrame);
    327             }
    328 
    329             // Wait for tiles to finish loading before beginning the fade
    330             watchUtils.whenFalseOnce(
    331                 layerView,
    332                 "updating",
    333                 function(updating) {
    334                     if (flag) {
    335                         requestAnimationFrame(incrementOpacityByFrame);
    336                     } else {
    337                         requestAnimationFrame(decrementOpacityByFrame);
    338                     }
    339                 }
    340             );
    341         });
    342     }
    343 
    344 }
    View Code

     

    2:首先根据视口内切圆的范围来查询,把构建了的要素缓存,在地图漫游的时候在缓存中查找,避免重复构建,移出视口内切圆范围的要素移除(缓存不清除),其实就和瓦片加载机制(行列号级别缓存)类似。还要限制级别,如果当级别很小的时候,视口内切圆中的数据量太多,会卡顿,所以这种方案最好是做达到一定级别,房屋图层渐变显示,反之渐变消失,代码中有。再用arcgis的featurelayer,symbol的polygon-3d、extrude来构建加载,其实featurelayer应该是开了work异步加载的,但是数据量也就能保证在2万左右,并且初始化的时候加载策略和3dTiles是类似的,看不全!是在用户漫游地图,放大平移的时候分批加载的。性能一般,样式控制不灵活,效果也不行。

      1 import GraphicsLayer from "@arcgis/core/layers/GraphicsLayer";
      2 import FeatureLayer from "@arcgis/core/layers/FeatureLayer";
      3 import Graphic from "@arcgis/core/Graphic";
      4 import Mesh from "@arcgis/core/geometry/Mesh";
      5 import Polygon from "@arcgis/core/geometry/Polygon";
      6 import Polyline from "@arcgis/core/geometry/Polyline";
      7 import Circle from "@arcgis/core/geometry/Circle";
      8 import * as watchUtils from "@arcgis/core/core/watchUtils";
      9 import * as geometryEngine from "@arcgis/core/geometry/geometryEngine";
     10 import * as webMercatorUtils from "@arcgis/core/geometry/support/webMercatorUtils";
     11 import mapx from '@/utils/mapUtils.js';
     12 
     13 export default class BMLayer {
     14     constructor(ops) {
     15         this.$url = ops.url;
     16         this.$view = ops.view;
     17         // this.$zoom = ops.zoom;
     18         this.$ofomatter = ops.ofomatter;
     19         this.$idkey = ops.idkey;
     20         this.$click = ops.click;
     21         this.$zfomatter = ops.zfomatter;
     22         this.$wfomatter = ops.wfomatter;
     23         this.$sfomatter = ops.sfomatter;
     24 
     25         this.setup()
     26     }
     27 
     28     setup() {
     29         this.layer = null;
     30         this.highlightSelect = null;
     31         this.preModels = {};
     32         this.ef = 1.2;
     33         this.autoLoad = false;
     34         this.circle = null;
     35         this.circleGraphic = null;
     36         this.rendering = false;
     37         this.maxZoom = 20;
     38         this.baseRadius = 700;
     39         this.factor = 0.66;
     40 
     41         this.layer = new GraphicsLayer();
     42         this.$view.map.add(this.layer);
     43 
     44         this.baselayer = new FeatureLayer({
     45             source: [],
     46             objectIdField: "ObjectID",
     47             geometryType: 'polygon',
     48             render: {
     49                 type: "simple",
     50                 symbol: this.getBaseSymbol()
     51             }
     52         });
     53         this.$view.map.add(this.baselayer);
     54 
     55         this.addEvents();
     56     }
     57 
     58     addModel(fs) {
     59         for (let key in this.preModels) {
     60             const m = this.preModels[key];
     61             // if (!this.$view.extent.intersects(m.geometry)) {
     62             const flag = geometryEngine.intersects(this.circle, m.geometry)
     63             if (!flag) {
     64                 this.layer.remove(m);
     65                 delete this.preModels[key]
     66             }
     67         }
     68         const per = 300;
     69         // console.log(fs.length)
     70         if (fs.length > per) {
     71             fs.length = per
     72         }
     73         let sid = setInterval(() => {
     74             if (fs.length === 0) {
     75                 this.rendering = false;
     76                 clearInterval(sid);
     77             }
     78             let i = 0;
     79             for (; i < fs.length; i++) {
     80                 const f = fs.pop()
     81                 if (f) {
     82                     const att = f.attributes;
     83                     const uid = att[this.$idkey];
     84 
     85                     if (this.preModels.hasOwnProperty(uid)) {
     86 
     87                     } else {
     88                         const z = this.$zfomatter(att)
     89                         let symbol;
     90                         if (this.$sfomatter) {
     91                             symbol = this.getBaseSymbol(z, this.$sfomatter(f));
     92                         } else {
     93                             symbol = this.getBaseSymbol(z);
     94                         }
     95                         const model = f;
     96                         model.symbol = symbol;
     97                         // this.layer.add(model);
     98                         this.baselayer.applyEdits({addFeatures: [model]})
     99 
    100                         this.preModels[uid] = model;
    101                     }
    102                 }
    103             }
    104         }, 25);
    105     }
    106 
    107     click(results, mapPoint) {
    108         if (results && results.length > 0) {
    109             var grah = results[0].graphic;
    110             if (grah.layer === this.layer) {
    111                 if (this.highlightSelect) {
    112                     this.highlightSelect.remove();
    113                 }
    114 
    115                 this.$view.whenLayerView(grah.layer).then(
    116                     layerView => {
    117                         this.highlightSelect = layerView.highlight(grah);
    118                     });
    119                 this.$click(grah, mapPoint, this.$view);
    120             } else {
    121                 if (this.highlightSelect) {
    122                     this.highlightSelect.remove();
    123                 }
    124                 // this.$view.popup.close()
    125             }
    126         } else {
    127             if (this.highlightSelect) {
    128                 this.highlightSelect.remove();
    129             }
    130             // this.$view.popup.close()
    131         }
    132     }
    133 
    134     clearHighlight() {
    135         if (this.highlightSelect) {
    136             this.highlightSelect.remove();
    137         }
    138     }
    139 
    140     setAutoLoad(v) {
    141         this.autoLoad = v
    142     }
    143 
    144     addEvents() {
    145         watchUtils.watch(this.$view, 'zoom', () => {
    146             if (this.$view.zoom > this.maxZoom) {
    147                 this.$view.zoom = this.maxZoom;
    148             }
    149         });
    150 
    151         watchUtils.whenTrue(this.$view, "stationary", () => {
    152             // console.log(this.$view.zoom)
    153             const flag = this.$ofomatter(this.$view);
    154             if (flag) {
    155                 if (!this.rendering) {
    156                     this.rendering = true;
    157                     if (this.autoLoad) {
    158                         this.loadData();
    159                     }
    160                 }
    161                 // this.layer.visible = true;
    162                 if (this.layer.opacity === 0) {
    163                     this.fadeVisibilityOn(this.$view, this.layer, true)
    164                 }
    165             } else {
    166                 // this.clearLayer();
    167                 this.rendering = false;
    168                 // this.layer.visible = false;
    169                 if (this.layer.opacity === 1) {
    170                     this.fadeVisibilityOn(this.$view, this.layer, false)
    171                 }
    172             }
    173         });
    174     }
    175 
    176     loadData() {
    177         var r = this.getRadius();
    178         var center = this.$view.center;
    179         const p = webMercatorUtils.xyToLngLat(center.x, center.y);
    180         p.z = 10;
    181         this.circle = new Circle({
    182             center: p,
    183             geodesic: true,
    184             numberOfPoints: 10,
    185             radius: r,
    186             radiusUnit: "meters"
    187         })
    188         // if(this.circleGraphic) {
    189         //     this.layer.remove(this.circleGraphic);
    190         // }
    191         // this.circleGraphic = new Graphic({
    192         //     geometry: this.circle,
    193         //     symbol: {
    194         //         type: "simple-fill",
    195         //         color: [51, 51, 204, 0.7],
    196         //         style: "solid",
    197         //         outline: {
    198         //             color: "white",
    199         //             width: 1
    200         //         }
    201         //     }
    202         // })
    203         // this.layer.add(this.circleGraphic);
    204 
    205         let where = ''
    206         if (this.$wfomatter) {
    207             where = this.$wfomatter();
    208         }
    209         mapx.queryTask(this.$url, {
    210             where: where,
    211             outSpatialReference: '4326',
    212             // geometry: this.$view.extent,
    213             geometry: this.circle,
    214             returnGeometry: true
    215         }).then(featureSet => {
    216             this.addModel(featureSet);
    217         }).catch(error => {})
    218     }
    219 
    220     clearLayer() {
    221         this.layer.removeAll();
    222         this.preModels = {};
    223     }
    224 
    225     getRadius() {
    226         const zoomMap = {
    227             '15': this.baseRadius / this.factor / this.factor,
    228             '16': this.baseRadius / this.factor,
    229             '17': this.baseRadius,
    230             '18': this.baseRadius * this.factor,
    231             '19': this.baseRadius * this.factor * this.factor,
    232             '20': this.baseRadius * this.factor * this.factor * this.factor,
    233             '21': this.baseRadius * this.factor * this.factor * this.factor * this.factor
    234         }
    235         const zoom = Math.round(this.$view.zoom)
    236         return zoomMap[zoom + '']
    237         // var extent = this.$view.extent;
    238         // var paths = [
    239         //     [
    240         //         [extent.xmin, extent.ymin],
    241         //         [extent.xmax, extent.ymax]
    242         //     ]
    243         // ];
    244         // var line = new Polyline({
    245         //     paths: paths,
    246         //     spatialReference: this.$view.spatialReference
    247         // });
    248         // var d = geometryEngine.geodesicLength(line, 'meters');
    249         // // var d = geometryEngine.planarLength(line, 'meters');
    250         // return d * 0.5 * this.ef;
    251     }
    252 
    253     getBaseSymbol(z, color = [224, 224, 224, 0.8]) {
    254         return {
    255             type: "polygon-3d",
    256             symbolLayers: [{
    257                 type: "extrude",
    258                 size: z,
    259                 material: {
    260                     color: color
    261                 },
    262                 edges: {
    263                     type: "solid",
    264                     size: 1.5,
    265                     color: [50, 50, 50, 0.5]
    266                     // type: "sketch",
    267                     // color: [50, 50, 50, 0.5],
    268                     // size: 1.5,
    269                     // extensionLength: 2
    270                 }
    271             }]
    272         }
    273     }
    274 
    275     fadeVisibilityOn(view, layer, flag) {
    276         let animating = true;
    277         let opacity = flag ? 0 : 1;
    278         // fade layer's opacity from 0 to
    279         // whichever value the user has configured
    280         const finalOpacity = flag ? 1 : 0;
    281         layer.opacity = opacity;
    282 
    283         view.whenLayerView(layer).then((layerView) => {
    284             function incrementOpacityByFrame() {
    285                 if (opacity >= finalOpacity && animating) {
    286                     layer.opacity = finalOpacity;
    287                     animating = false;
    288                     return;
    289                 }
    290 
    291                 layer.opacity = opacity;
    292                 opacity += 0.07;
    293 
    294                 requestAnimationFrame(incrementOpacityByFrame);
    295             }
    296 
    297             function decrementOpacityByFrame() {
    298                 if (opacity <= finalOpacity && animating) {
    299                     layer.opacity = finalOpacity;
    300                     animating = false;
    301                     return;
    302                 }
    303 
    304                 layer.opacity = opacity;
    305                 opacity -= 0.07;
    306 
    307                 requestAnimationFrame(decrementOpacityByFrame);
    308             }
    309 
    310             // Wait for tiles to finish loading before beginning the fade
    311             watchUtils.whenFalseOnce(
    312                 layerView,
    313                 "updating",
    314                 function(updating) {
    315                     if (flag) {
    316                         requestAnimationFrame(incrementOpacityByFrame);
    317                     } else {
    318                         requestAnimationFrame(decrementOpacityByFrame);
    319                     }
    320                 }
    321             );
    322         });
    323     }
    324 
    325 }
    View Code

     

    3:首先把漫游策略改了,方案1、2是漫游中加载并缓存,现在直接在初始化的时候给出进度条,加载所有数据(6万+),肯定是不能一口气去查询加载的,我还是启动一个延迟加载的策略,在地图上分区域分布队列加载,尽量在每一帧里面分摊开销。然后完全舍弃arcgis的要素渲染,改用threejs来绘制,arcgis给出了一个接口externalRenderers,这个很重要,可以集成第三方3D引擎,还需要注意的是坐标系球面和平面的坐标转换,也就是说arcgis SceneView viewingMode=”local“和”globle“是不一样的,如果是局部展示,不需要展示三维球的话,建议使用local模式,这样会处理简单一些。threejs这边使用Shape,ExtrudeGeometry,Mesh来构建要素,但是如果仅仅是这样去渲染,当6万+个建筑物在地图上是会卡顿的,因为对象太多了,那么我们是否可以做一个合并操作呢,把6万+个要素按区域合并,也就是合并geometry咯,一开始在网上找了一个mergeBufferGeometries算法,后来发现threejs API里面有BufferGeometryUtils.mergeBufferGeometries,感觉threejs计算速度快一点。合并之后在加载到图层上,那么事实上,比如全市是15个区,那就只有15个Mesh,当然不卡了,满帧跑。不过相比大家也会发现一个问题,就是当要和建筑物交互的时候,就获取不到点击的Mesh了,这个问题我会继续区研究一下怎么改进。最后,效果和样式就不用担心了,threejs自带的Material就很丰富,不行还有Shader着色器,动效也方便,在updateModels里面随便操作(threejs的render事件,我封装了),归根结底,剩下的就是threejs的能力展现了。

     

    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         });
     72         this.renderer.setPixelRatio(window.devicePixelRatio); // 设置设备像素比。通常用于避免HiDPI设备上绘图模糊
     73         this.renderer.setViewport(0, 0, this.view.width, this.view.height); // 视口大小设置
     74         
     75         // 防止Three.js清除ArcGIS JS API提供的缓冲区。
     76         this.renderer.autoClearDepth = false; // 定义renderer是否清除深度缓存
     77         this.renderer.autoClearStencil = false; // 定义renderer是否清除模板缓存
     78         this.renderer.autoClearColor = false; // 定义renderer是否清除颜色缓存
     79         // this.renderer.autoClear = false;
     80         
     81         // ArcGIS JS API渲染自定义离屏缓冲区,而不是默认的帧缓冲区。
     82         // 我们必须将这段代码注入到three.js运行时中,以便绑定这些缓冲区而不是默认的缓冲区。
     83         const originalSetRenderTarget = this.renderer.setRenderTarget.bind(
     84             this.renderer
     85         );
     86         this.renderer.setRenderTarget = target => {
     87             originalSetRenderTarget(target);
     88             if (target == null) {
     89                 // 绑定外部渲染器应该渲染到的颜色和深度缓冲区
     90                 context.bindRenderTarget();
     91             }
     92         };
     93         
     94         this.addModels(context);
     95 
     96         context.resetWebGLState();
     97     }
     98 
     99     createRender(context) {
    100         const cam = context.camera;
    101         this.camera.position.set(cam.eye[0], cam.eye[1], cam.eye[2]);
    102         this.camera.up.set(cam.up[0], cam.up[1], cam.up[2]);
    103         this.camera.lookAt(
    104             new THREE.Vector3(cam.center[0], cam.center[1], cam.center[2])
    105         );
    106         this.camera.near = 1;
    107         this.camera.far = 100;
    108 
    109         // 投影矩阵可以直接复制
    110         this.camera.projectionMatrix.fromArray(cam.projectionMatrix);
    111         
    112         this.updateModels(context);
    113 
    114         this.renderer.state.reset();
    115 
    116         context.bindRenderTarget();
    117 
    118         this.renderer.render(this.scene, this.camera);
    119 
    120         // 请求重绘视图。
    121         externalRenderers.requestRender(this.view);
    122 
    123         // cleanup
    124         context.resetWebGLState();
    125     }
    126     
    127     //经纬度坐标转成三维空间坐标
    128     lngLatToXY(view, points) {
    129     
    130         let vector3List; // 顶点数组
    131     
    132         let pointXYs;
    133     
    134     
    135         // 计算顶点
    136         let transform = new THREE.Matrix4(); // 变换矩阵
    137         let transformation = new Array(16);
    138     
    139         // 将经纬度坐标转换为xy值\
    140         let pointXY = webMercatorUtils.lngLatToXY(points[0], points[1]);
    141     
    142         // 先转换高度为0的点
    143         transform.fromArray(
    144             externalRenderers.renderCoordinateTransformAt(
    145                 view,
    146                 [pointXY[0], pointXY[1], points[
    147                     2]], // 坐标在地面上的点[x值, y值, 高度值]
    148                 view.spatialReference,
    149                 transformation
    150             )
    151         );
    152     
    153         pointXYs = pointXY;
    154     
    155         vector3List =
    156             new THREE.Vector3(
    157                 transform.elements[12],
    158                 transform.elements[13],
    159                 transform.elements[14]
    160             )
    161     
    162         return {
    163             vector3List: vector3List,
    164             pointXYs: pointXYs
    165         };
    166     }
    167     
    168     setLight() {
    169         console.log('setLight')
    170         let ambient = new THREE.AmbientLight(0xffffff, 0.7);
    171         this.scene.add(ambient);
    172         let directionalLight = new THREE.DirectionalLight(0xffffff, 0.7);
    173         directionalLight.position.set(100, 300, 200);
    174         this.scene.add(directionalLight);
    175     }
    176     
    177     addModels(context) {
    178         console.log('addModels')
    179     }
    180     
    181     updateModels(context) {
    182         // console.log('updateModels')
    183     }
    184     
    185 }
    View Code

    BuildingLayerExt:

      1 import mapx from '@/utils/mapUtils.js';
      2 
      3 import * as THREE from 'three'
      4 import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
      5 import ExternalRendererLayer from './ExternalRendererLayer.js'
      6 
      7 import GraphicsLayer from "@arcgis/core/layers/GraphicsLayer";
      8 import FeatureLayer from "@arcgis/core/layers/FeatureLayer";
      9 import Graphic from "@arcgis/core/Graphic";
     10 import SpatialReference from '@arcgis/core/geometry/SpatialReference'
     11 import * as externalRenderers from "@arcgis/core/views/3d/externalRenderers"
     12 import Polygon from "@arcgis/core/geometry/Polygon";
     13 import Polyline from "@arcgis/core/geometry/Polyline";
     14 import Circle from "@arcgis/core/geometry/Circle";
     15 import * as watchUtils from "@arcgis/core/core/watchUtils";
     16 import * as geometryEngine from "@arcgis/core/geometry/geometryEngine";
     17 import * as webMercatorUtils from "@arcgis/core/geometry/support/webMercatorUtils";
     18 
     19 import { getBuildings } from '@/api';
     20 
     21 const EF = 1;
     22 const UID = 'FID'; //OBJECTID
     23 const R = 0.8;
     24 const LEVEL = 16;
     25 const HEIGHT = 40;
     26 const INCREASE = 30;
     27 
     28 const JBMS = [
     29     420106001,
     30     420106002,
     31     420106003,
     32     420106005,
     33     420106006,
     34     420106007,
     35     420106008,
     36     420106009,
     37     420106010,
     38     420106011,
     39     420106012,
     40     420106013,
     41     420106014,
     42     420106015
     43 ];
     44 
     45 export default class BuildingLayerExt extends ExternalRendererLayer {
     46     constructor({
     47         view,
     48         options
     49     }) {
     50         super({
     51             view,
     52             options
     53         })
     54     }
     55 
     56     setup() {
     57         // this.circleGraphic = null;
     58         // this.layer = new GraphicsLayer();
     59 
     60         this.cacheObjects = {};
     61 
     62         this.group = new THREE.Group();
     63 
     64         // this.material = new THREE.MeshLambertMaterial({
     65         //     transparent: true,
     66         //     opacity: 0.9,
     67         //     color: 0xFFFFFF
     68         // }); //材质对象Material
     69 
     70         // 顶点着色器
     71         const vertexShader = `
     72                      // 向片元着色器传递顶点位置数据
     73                      varying vec3 v_position;
     74                      void main () {
     75                          v_position = position;
     76                          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
     77                      }
     78                  `;
     79         // 片元着色器
     80         const fragmentShader = `
     81                      // 接收顶点着色器传递的顶点位置数据
     82                      varying vec3 v_position;
     83                 
     84                      // 接收js传入的值
     85                      uniform float u_time;
     86                      uniform vec3 u_size;
     87                      uniform vec3 u_flow;
     88                      uniform vec3 u_color;
     89                      uniform vec3 u_flowColor;
     90                      uniform vec3 u_topColor;
     91                 
     92                      void main () {
     93                          // 给建筑设置从上到下的渐变颜色
     94                          float indexPct = v_position.z / u_size.z;
     95                          vec3 color = mix(u_color, u_topColor,indexPct);
     96                          // // 根据时间和速度计算出当前扫描点的位置, 以上顶点为准
     97                          // float flowTop = mod(u_flow.z * u_time, u_size.z);
     98                          // // 判断当前点是否在扫描范围内
     99                          // if (flowTop > v_position.z && flowTop - u_flow.z < v_position.z) {
    100                          //     // 扫描范围内的位置设置从上到下的渐变颜色
    101                          //     float flowPct = (u_flow.z - ( flowTop -  v_position.z)) / u_flow.z;
    102                          //     color = mix(color ,u_flowColor, flowPct);
    103                          // }
    104                          gl_FragColor = vec4(color, 0.8);
    105                      }
    106                  `;
    107 
    108         const ratio = {
    109             value: 0
    110         }
    111 
    112         // 楼宇扫描相关配置数据
    113         const flowData = {
    114             boxSize: { // 建筑群包围盒的尺寸
    115                 x: 0,
    116                 y: 0,
    117                 z: HEIGHT
    118             },
    119             flowConf: {
    120                 x: 1, // 开关 1 表示开始
    121                 y: 20, // 范围
    122                 z: 100 // 速度
    123             },
    124             color: "#000000", // 建筑颜色
    125             flowColor: "#ffffff", // 扫描颜色
    126             topColor: '#409eff' // 顶部颜色
    127         }
    128 
    129         this.material = new THREE.ShaderMaterial({
    130             transparent: true,
    131             uniforms: {
    132                 u_time: ratio,
    133                 u_size: {
    134                     value: flowData.boxSize
    135                 },
    136                 u_flow: {
    137                     value: flowData.flowConf
    138                 },
    139                 u_color: {
    140                     value: new THREE.Color(flowData.color)
    141                 },
    142                 u_flowColor: {
    143                     value: new THREE.Color(flowData.flowColor)
    144                 },
    145                 u_topColor: {
    146                     value: new THREE.Color(flowData.topColor)
    147                 }
    148             },
    149             vertexShader,
    150             fragmentShader
    151         });
    152 
    153         watchUtils.whenTrue(this.view, "stationary", () => {
    154             if (this.options.zoomChange) {
    155                 this.options.zoomChange(this.view.zoom);
    156             }
    157             // if (this.view.zoom >= LEVEL) {
    158             //     this.group.visible = true;
    159             //     this.loadData();
    160             // } else {
    161             //     this.group.visible = false;
    162             // }
    163         });
    164     }
    165 
    166     addModels(context) {
    167         super.addModels(context);
    168 
    169         this.loadData();
    170     }
    171 
    172     updateModels(context) {
    173         super.updateModels(context);
    174 
    175         // this.objects.forEach(obj => {
    176         //     obj.material.uniforms.time.value += 0.01;
    177         // })
    178 
    179         // this.group.children.forEach(mesh => {
    180         //     if (mesh.scale.z >= 1) {
    181         //         mesh.scale.z = 1;
    182         //     } else {
    183         //         mesh.scale.z += 0.02;
    184         //     }
    185         // })
    186     }
    187 
    188     loadData() {
    189         let count = 0;
    190         // let index = 0;
    191         this._loadData(featureSet => {
    192             // console.log(index);
    193             // index++;
    194             // console.log('fz:' + featureSet.length)
    195             // console.log(count += featureSet.length)
    196             
    197             let _objects = []
    198             featureSet.forEach(feature => {
    199                 // this._validateModel(feature);
    200                 const obj = this._addModel(feature);
    201                 _objects.push(obj.geometry);
    202             })
    203             
    204             console.log(_objects.length)
    205             
    206             console.time("render building");
    207             const mergeGeometry = BufferGeometryUtils.mergeBufferGeometries(_objects);
    208             // const mergeGeometry = this.mergeBufferGeometry(_objects);
    209             console.timeEnd("render building");
    210             const mergeMesh = new THREE.Mesh(mergeGeometry, this.material);
    211             // mergeMesh.scale.z = 0;
    212             
    213             this.group.add(mergeMesh);
    214             console.log('this.group.children.length2:' + this.group.children.length);
    215             
    216             this.scene.add(this.group); //网格模型添加到场景中
    217         })
    218         // http://10.102.109.88:9530/?type=qzx
    219         
    220         // const url = config.mapservice[1].base_url + config.mapservice[1].house_url;
    221         // // const url = 'http://10.34.4.103:8010/ServiceAdapter/Map/%E6%88%BF%E5%B1%8B/15d4b9815cf7420da111307850d2049f/0';
    222         // // const url = 'http://10.100.0.132:6080/arcgis/rest/services/wuchang_gim/gim_region/MapServer/0';
    223         // JBMS.forEach(jbm => {
    224         //     mapx.queryTask(url, {
    225         //         where: `JBM='${jbm}'`,
    226         //         outSpatialReference: '4326',
    227         //         // geometry: this.view.extent,
    228         //         // geometry: this.circle,
    229         //         returnGeometry: true
    230         //     }).then(featureSet => {
    231         //         console.log('fz:' + featureSet.length)
    232 
    233         //         console.time("render building");
    234                 
    235         //         let _objects = []
    236         //         featureSet.forEach(feature => {
    237         //             // this._validateModel(feature);
    238         //             const obj = this._addModel(feature);
    239         //             _objects.push(obj.geometry);
    240         //         })
    241         //         const mergeGeometry = BufferGeometryUtils.mergeBufferGeometries(_objects);
    242         //         // const mergeGeometry = this.mergeBufferGeometry(_objects);
    243         //         const mergeMesh = new THREE.Mesh(mergeGeometry, this.material);
    244         //         this.group.add(mergeMesh);
    245 
    246         //         console.timeEnd("render building");
    247         //         console.log('this.group.children.length2:' + this.group.children.length);
    248 
    249         //         this.scene.add(this.group); //网格模型添加到场景中
    250 
    251         //     }).catch(error => {})
    252         // })
    253     }
    254     
    255     _loadData(callback) {
    256         //循环并联本地查询
    257         JBMS.forEach(jbm => {
    258             getBuildings(jbm).then(res => {
    259                 callback(res.data.features)
    260                 console.log(res.data.features)
    261             })
    262         })
    263         
    264         return
    265         
    266         const url = config.mapservice[1].base_url + config.mapservice[1].house_url;
    267         // const url = 'http://10.34.4.103:8010/ServiceAdapter/Map/%E6%88%BF%E5%B1%8B/15d4b9815cf7420da111307850d2049f/0';
    268         // const url = 'http://10.100.0.132:6080/arcgis/rest/services/wuchang_gim/gim_region/MapServer/0';
    269         
    270         //循环并联分发查询
    271         JBMS.forEach(jbm => {
    272             mapx.queryTask(url, {
    273                 where: `JBM='${jbm}'`,
    274                 outSpatialReference: '4326',
    275                 // geometry: this.view.extent,
    276                 // geometry: this.circle,
    277                 returnGeometry: true
    278             }).then(featureSet => {
    279                 callback(featureSet)
    280             }).catch(error => {})
    281         })
    282         
    283         return
    284         
    285         //递归串联分发查询
    286         let index= 0;
    287         function query() {
    288             mapx.queryTask(url, {
    289                 where: `JBM='${JBMS[index]}'`,
    290                 outSpatialReference: '4326',
    291                 // geometry: this.view.extent,
    292                 // geometry: this.circle,
    293                 returnGeometry: true
    294             }).then(featureSet => {
    295                 callback(featureSet)
    296                 index++;
    297                 if(index < JBMS.length) {
    298                     // const sid = setTimeout(() => {
    299                         // clearTimeout(sid);
    300                         query();
    301                     // }, 2000)
    302                 }
    303             }).catch(error => {})
    304         }
    305         query();
    306     }
    307 
    308     // _validateModel(feature) {
    309     //     for (let key in this.cacheObjects) {
    310     //         const m = this.cacheObjects[key];
    311     //         const flag = this.view.extent.intersects(m.geometry)
    312     //         // const flag = geometryEngine.intersects(this.circle, m.geometry)
    313     //         if (!flag) {
    314     //             this.group.remove(m);
    315     //             delete this.cacheObjects[key]
    316     //         }
    317     //     }
    318     // }
    319 
    320     _addModel(feature) {
    321         //处理缓存
    322         const uid = feature.attributes[UID];
    323         if (this.cacheObjects.hasOwnProperty(uid)) {
    324             // this.cacheObjects[uid].visible = true;
    325         } else {
    326             this.cacheObjects[uid] = feature;
    327 
    328             let height = HEIGHT;
    329             const points = feature.geometry.rings[0];
    330             const htemp = feature.attributes['高度'];
    331             if (htemp) {
    332                 height = htemp + INCREASE;
    333             }
    334 
    335             let vertices = [];
    336             for (let i = 0; i < points.length; i++) {
    337                 let p = points[i];
    338                 let pointXYZ = this.lngLatToXY(this.view, [p[0], p[1], 0]);
    339                 vertices.push(pointXYZ);
    340             }
    341 
    342             const shape = new THREE.Shape();
    343             for (let i = 0; i < vertices.length; i++) {
    344                 let v = vertices[i].vector3List;
    345                 if (i === 0) {
    346                     shape.moveTo(v.x, v.y);
    347                 }
    348                 shape.lineTo(v.x, v.y);
    349             }
    350 
    351             const extrudeSettings = {
    352                 steps: 2,
    353                 depth: height,
    354                 bevelEnabled: true,
    355                 bevelThickness: 1,
    356                 bevelSize: 1,
    357                 bevelOffset: 0,
    358                 bevelSegments: 1
    359             };
    360 
    361             const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
    362             const mesh = new THREE.Mesh(geometry, this.material); //网格模型对象Mesh 
    363 
    364             // const edges = new THREE.EdgesGeometry(geometry);
    365             // const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({
    366             //     color: 0x000000,
    367             //     linewidth: 1
    368             // }));
    369 
    370             // this.group.add(mesh);
    371             // this.group.add(line);
    372             this.objects.push(mesh);
    373 
    374             // mesh.scale.z = 0;
    375             
    376             return mesh;
    377         }
    378     }
    379 
    380     getRadius() {
    381         var extent = this.view.extent;
    382         var lt = webMercatorUtils.xyToLngLat(extent.xmin, extent.ymin);
    383         var rb = webMercatorUtils.xyToLngLat(extent.xmax, extent.ymax);
    384         var paths = [
    385             [
    386                 lt,
    387                 rb
    388             ]
    389         ];
    390         var line = new Polyline({
    391             paths: paths,
    392             spatialReference: {
    393                 wkid: '4326'
    394             }
    395         });
    396         var d = geometryEngine.geodesicLength(line, 'meters');
    397         // var d = geometryEngine.planarLength(line, 'meters');
    398         return d * 0.5 * EF;
    399     }
    400 
    401     mergeBufferGeometry(objects) {
    402         const sumPosArr = new Array();
    403         const sumNormArr = new Array();
    404         const sumUvArr = new Array();
    405 
    406         const modelGeometry = new THREE.BufferGeometry();
    407 
    408         let sumPosCursor = 0;
    409         let sumNormCursor = 0;
    410         let sumUvCursor = 0;
    411 
    412         let startGroupCount = 0;
    413         let lastGroupCount = 0;
    414 
    415         for (let a = 0; a < objects.length; a++) {
    416             const posAttArr = objects[a].geometry.getAttribute('position').array;
    417 
    418             for (let b = 0; b < posAttArr.length; b++) {
    419                 sumPosArr[b + sumPosCursor] = posAttArr[b];
    420             }
    421 
    422             sumPosCursor += posAttArr.length;
    423 
    424 
    425             const numAttArr = objects[a].geometry.getAttribute('normal').array;
    426 
    427             for (let b = 0; b < numAttArr.length; b++) {
    428                 sumNormArr[b + sumNormCursor] = numAttArr[b];
    429             }
    430 
    431             sumNormCursor += numAttArr.length;
    432 
    433 
    434             const uvAttArr = objects[a].geometry.getAttribute('uv').array;
    435 
    436             for (let b = 0; b < uvAttArr.length; b++) {
    437                 sumUvArr[b + sumUvCursor] = uvAttArr[b];
    438             }
    439 
    440             sumUvCursor += uvAttArr.length;
    441 
    442             const groupArr = objects[a].geometry.groups;
    443 
    444             for (let b = 0; b < groupArr.length; b++) {
    445                 startGroupCount = lastGroupCount
    446                 modelGeometry.addGroup(startGroupCount, groupArr[b].count, groupArr[b].materialIndex)
    447                 lastGroupCount = startGroupCount + groupArr[b].count
    448             }
    449         }
    450 
    451         modelGeometry.setAttribute('position', new THREE.Float32BufferAttribute(sumPosArr, 3));
    452         sumNormArr.length && modelGeometry.setAttribute('normal', new THREE.Float32BufferAttribute(sumNormArr, 3));
    453         sumUvArr.length && modelGeometry.setAttribute('uv', new THREE.Float32BufferAttribute(sumUvArr, 2));
    454 
    455         return modelGeometry
    456     }
    457 }
    View Code

     

    调用案例:

    复制代码
     1 import BuildingLayer from './core/BuildingLayerExt.js';
     2 
     3 this.buildingLayer = new BuildingLayer({
     4                         view: v,
     5                         options: {
     6                             zoomChange: val => {
     7                                 this.setMapZoom(val);
     8                             }
     9                         }
    10                     });
    11                     this.buildingLayer.apply();
    复制代码

     

  • 相关阅读:
    【Tensorflow 2.12 电影推荐系统之排序模型】
    PICO《轻世界》体验:随心畅玩,洒脱创作,潜力无限
    元数据的前世今生
    数字孪生技术打造智慧矿山可视化解决方案
    React简介
    TypeScript - 枚举 - 数字枚举
    前缀和以及哈希表优化
    Babylonjs学习笔记(二)——创建基本材质
    父子项目打包发布至私仓库
    echarts让设置legend宽度不生效
  • 原文地址:https://www.cnblogs.com/loveFlex/p/17665427.html