本文适合对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引擎,还需要注意的是坐标系球面和平面的坐标转换,也就是说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 }
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 }
调用案例:
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();