Binyy Wuhan

i love Flex i come from Wuhan

导航

数据可视化【原创】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();

 

posted on 2023-08-29 17:20  Binyy_Wuhan  阅读(1722)  评论(0编辑  收藏  举报