- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
本文适合对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 }
。
效果图:
。
最后此篇关于数据可视化【原创】vue+arcgis+threejs实现海量建筑物房屋渲染,性能优化的文章就讲到这里了,如果你想了解更多关于数据可视化【原创】vue+arcgis+threejs实现海量建筑物房屋渲染,性能优化的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
很难说出这里问的是什么。这个问题是含糊的、模糊的、不完整的、过于宽泛的或修辞性的,无法以目前的形式得到合理的回答。如需帮助澄清此问题以便重新打开它,visit the help center 。
目录 内置的高亮节点 自定义高亮 自定义高亮时保持原始颜色 总结 案例完整代码 通过官方文档,可知高
目录 32.go.Palette 一排放两个 33.go.Palette 基本用法 34.创建自己指向自己的连线 35.设置不同的 groupTemplate 和
目录 41.监听连线拖拽结束后的事件 42.监听画布的修改事件 43.监听节点被 del 删除后回调事件(用于实现调用接口做一些真实的删除操作) 44.监听节点鼠标
织梦初秋 那是一个宜人的初秋午后,阳光透过窗户洒在书桌上,我轻轻地拂去被阳光映照出的尘屑,伸了个懒腰。哎呀,这个世界真是奇妙啊,想到什么就能用代码实现,就像笔尖上点燃的火花。 思索的起点 我一直对天气
曲径通幽,古木参天 时光匆匆,不经意间已是2023年的秋季。我身处在这个充满朝气和变革的时代,每天都充满了新的科技突破和创新。而当我想起曾经努力学习的Python编程语言时,心中涌动着一股热情,渴望将
我有一个堆积条形图,由一个 bool 字段分割。这会导致图例显示为两种颜色(很酷!)但图例具有以下值:true 和 false。对于读者来说,什么是真或假意味着什么是没有上下文的。 在这种情况下,字段
我想在 R 中做一个简单的一阶马尔可夫链。我知道有像 MCMC 这样的包,但找不到一个以图形方式显示它的包。这甚至可能吗?如果给定一个转换矩阵和一个初始状态,那将会很好,人们可以直观地看到通过马尔可夫
我是 tableau 的新手,我有以下可视化,这是链接: My visualization 我的问题是我不知道如何在一个仪表板中添加多个仪表板作为选项卡。在我的可视化中,有三个仪表板“Nota tot
我建立类似自动VJ程序的东西。我有2个网络摄像头发出的2个incomig视频信号和一些可视化效果(目前2个,但我想要更多)。我有一个以dB为单位的传入音频信号音量,以bpm为单位。我需要的是视频输出的
我需要可视化的东西,并想要求一些提示和教程。或者使用哪种技术(Cocos2D、OpenGL、Quartz,...) 这里有人在 iOS 设备上做过可视化吗? 它是关于移动物体、褪色、粒子等等…… 任何
我对 Graphviz 越来越熟悉,想知道是否可以生成如下所示的图表/图表(不确定你叫它什么)。如果没有,有人知道什么是好的开源框架吗? (首选,C++,Java 或 Python)。 最佳答案 根据
问题很简单——我真的很喜欢用 UIStackView 来组织 UI。但是,我在测试应用程序中看不到 UIStackView 边界。当 UI 元素不是预期的时候,我需要花很多时间来调试。在网上搜索,我找
例如,我可以通过以下方式分配内存时的情况: Position* arr1 = new Position[5]; Position 是我程序中的一个类,它描述了具有 x 和 y 值的位置点。 堆栈上会有
关闭。这个问题不符合Stack Overflow guidelines .它目前不接受答案。 我们不允许提问寻求书籍、工具、软件库等的推荐。您可以编辑问题,以便用事实和引用来回答。 关闭 5 年前。
我最近一直在处理许多半复杂的 XSD,我想知道:有哪些更好的工具可以处理 XML 模式?有没有图形工具? 独立的或基于 Eclipse 的是理想的选择,因为我们不是 .net 商店。 最佳答案 我找到
通过一段时间的使用和学习,对G6有了更一步的经验,这篇博文主要从以下几个小功能着手介绍,文章最后会给出完整的demo代码。 目录 1. 树图的基本布局和
三维数据的获取方式 RGBD相机和深度图 代码展示:在pcl中,把点云转为深度图,并保存和可视化 三维数据的获取方式 在计算机视觉和遥感领域,点云可以通过四种主要的技术获得, (1)根据图像衍生而得,
代码 library(igraph) g <- graph.tree(n = 2 ^ 3 - 1, children = 2) node_labels <- c("", "Group A", "Gro
我正在关注 this tutorial并创建了一个这样的图表: from dask.threaded import get from operator import add dsk = { 'x
我是一名优秀的程序员,十分优秀!