数据可视化【原创】vue+arcgis+threejs 实现海量建筑物房屋渲染,性能优化
阅读原文时间:2023年08月30日阅读:47

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

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 }

3:首先把漫游策略改了,方案1、2是漫游中加载并缓存,现在直接在初始化的时候给出进度条,加载所有数据(6万+),肯定是不能一口气去查询加载的,我还是启动一个延迟加载的策略,在地图上分区域分布队列加载,尽量在每一帧里面分摊开销。然后完全舍弃arcgis的要素渲染,改用threejs来绘制,arcgis给出了一个接口externalRenderers,这个很重要,可以集成第三方3D引擎。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 }

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 }

效果图: