前言
前段时间系统性地学习了国内知名GIS平台的三维应用解决方案,受益匪浅,准备分几期对所学内容进行整理归纳和分享,今天来讲讲城市3D模型的处理方案。
城市3D模型图层在Web GIS平台中能够对地面物体进行单独选中、查询、空间分析等操作,是属于比较普遍的功能需求。这样的图层一般不会只有单一的数据来源,而是个集成了倾斜投影、BIM建模、动态参数建模的混合型图层,并且需要对模型进行适当的处理优化以提升其性能,因此在GIS平台不会选择支持单独地选中地图,我们需要对模型进行拆分——即单体化处理。
对建筑模型单体化处理通常有三种方案,各有其优点和适用场景,对比如下:
方案 | 实现原理 | 优势 | 缺陷 |
---|---|---|---|
切割单体化 | 将三维模型与二维面进行切割操作成为单独可操作对象 | 能够实现非常精细的单体化效果,适用于精细度要求高的场景 | 数据处理量大,对计算机性能要求较高 |
ID 单体化 | 预先为每个三维模型对象和二维矢量数据分配一个可关联的 ID | 数据处理相对简单,不需要进行复杂的几何运算,适用于数据量相对较小、模型结构相对简单的场景 | 可能会出现 ID 管理困难的情况,且对于模型的几何形状变化适应性较差 |
动态单体化 | 实时判断鼠标点击或选择操作的位置与三维模型的关系,动态地将选中的部分从整体模型中分离出来进行单独操作 | 无需预先处理数据,可根据用户的交互实时进行单体化操作,适用于需要快速响应用户交互的场景 | 对计算机的图形处理能力和性能要求较高 |
在这些方案中,动态单体化无需对模型进行预处理,依数据而变化使用较为灵活,接下来我们通过一个简单的例子来演示该方案的整体实现过程,源代码在这里供下载交流。
需求分析
假设我们拿到一个城市行政区域内的三维建筑数据,对里面该区域里面的几栋关键型建筑进行操作管理,我们需要把建筑模型放置到对应的地图位置上,可宏观地浏览模型;支持通过鼠标选中建筑的楼层,查看当前楼层的相关数据;楼层数据支持动态变更,且可以进行结构性存储。因此我们得到以下功能需求:
- 在Web地图上建立3D区域模型图层
- 根据当前光标位置动态高亮楼层,并展示楼层基本信息
- 建筑单体化数据为通用geoJSON格式,方便直接转换为csv或导入数据库
技术栈说明
工具名称 | 版本 | 用途 |
---|---|---|
高德地图 JSAPI | 2.0 | 为GIS平台提供基础底图和服务 |
three.js | 0.157 | 主流webGL引擎之一,负责实现展示层面的功能 |
QGIS | 3.32.3 | GIS数据处理工具,用于处理本文的矢量化数据 |
cesiumlab | 3.1.11 | 三维数据处理工具集,用于将模型转换为互联网可用的3DTiles |
blender | 3.6 | 模型处理工具,用于对BIM模型进行最简单的预处理 |
实现步骤
制作3DTiles
城市级的三维模型通常以无人机倾斜投影获取到的数据最为快捷,数据格式为OSGB且体积巨大不利于分享,由于手头没有合适的倾斜投影数据,我们以一个小型的BIM模型为例进行三维瓦片化处理也是一样的。
-
模型预处理。从sketchfab寻找到一个合适的建筑模型,下载其FBX格式并导入到模型处理工具(C4D、blender等)进行简单的预处理,调整模型的大小、重置坐标轴原点的位置到模型的几何中心,然后导出带材质的FBX模型备用,这里blender如何带材质地导出模型有一些技巧。
-
启动cesiumlab,点击“通用模型切片”选项,选择预处理好的模型,指定它的地理位置(ENU: 维度,经度),点击确认
-
在最后的“数据存储”设置原始坐标为打开、存储类型为散列(最终输出多个文件)、输出路径,提交处理等待3DTiles生成
-
生成过程结束后我们来到分发服务选项,点击chrome的图标就能够进入3DTiles的预览了,注意看路径这一列,这里面包含了入口文件tileset.json的两个路径(文件存储目录和静态资源服务地址),后面开发中我们会用到它。
-
至此模型准备完毕,我们可以把输出出的3Tiles目录放到开发i工程中,也可以单独部署为静态资源服务,保证tileset.json可访问即可。
-
开发3DTiles图层,详细的教程之前已经分享过了,这里直接上代码。
// 默认地图状态 const mapConf = { name: '虚拟小区', tilesURL: '../static/tiles/small-town/tileset.json', //... } // 添加3DTiles图层 async function initTilesLayer() { const layer = new TilesLayer({ container, id: 'tilesLayer', map: getMap(), center: [113.536206, 22.799285], zooms: [4, 22], zoom: mapConf.zoom, tilesURL: mapConf.tilesURL, alone: false, interact: false }) layer.on('complete', ({ scene }) => { // 调整模型的亮度 const aLight = new THREE.AmbientLight(0xffffff, 3.0) scene.add(aLight) }) layerManger.add(layer) }
-
这一阶段实现的效果如下
创建单体化数据
-
使用QGIS处理矢量数据,绘制建筑模型轮廓的矢量面。由于本示例是虚拟的,我们需要自己创建矢量数据,把上一个步骤完成的内容高空垂直截图导入到QGIS上配准位置,作为描绘的参考图。
-
创建形状文件图层,进入编辑模式绘制建筑轮廓
-
选择图层右键打开属性表,开始编辑每个建筑的基础数据,导出为monobuildingexample1.geojson
-
对关键建筑“商业办公楼A”和“商业办公楼B”的楼层数据进行进一步编辑(bottomAltitude为每层楼的离地高度,extendAltitude为楼层高度),这块数据与GIS无关,我直接用wps图表去做了,完成后将csv文件转换成为json格式,然后与monobuildingexample1.geojson做一个组合,得到最终的geoJSON数据。
开发动态单体化图层
底座和数据准备好,终于可以进行动态单体化图层开发了,实现原理其实非常简单,根据上一步获得的建筑矢量和楼层高度数据,我们就可以在于模型匹配的地理位置上创建若干个“罩住”楼栋模型的盒状网格体,并监听网格体的鼠标拾取状态,即可实现楼层单体化交互。
-
我们的数据来自monobuildingexample1.geojson,生成每个楼层侧面包围盒的核心代码如下,通过path数据和bottomAltitued、extendAltitude就能得到网格体的所有顶点。
/** * 根据路线创建侧面几何面 * @param {Array} path [[x,y],[x,y],[x,y]...] 路线数据 * @param {Number} height 几何面高度,默认为0 * @returns {THREE.BufferGeometry} */ createSideGeometry (path, region) { if (path instanceof Array === false) { throw 'createSideGeometry: path must be array' } const { id, bottomAltitude, extendAltitude } = region // 保持path的路线是闭合的 if (path[0].toString() !== path[path.length - 1].toString()) { path.push(path[0]) } const vec3List = [] // 顶点数组 let faceList = [] // 三角面数组 let faceVertexUvs = [] // 面的UV层队列,用于纹理和几何信息映射 const t0 = [0, 0] const t1 = [1, 0] const t2 = [1, 1] const t3 = [0, 1] for (let i = 0; i < path.length; i++) { const [x1, y1] = path[i] vec3List.push([x1, y1, bottomAltitude]) vec3List.push([x1, y1, bottomAltitude + extendAltitude]) } for (let i = 0; i < vec3List.length - 2; i++) { if (i % 2 === 0) { // 下三角 faceList = [ ...faceList, ...vec3List[i], ...vec3List[i + 2], ...vec3List[i + 1] ] // UV faceVertexUvs = [...faceVertexUvs, ...t0, ...t1, ...t3] } else { // 上三角 faceList = [ ...faceList, ...vec3List[i], ...vec3List[i + 1], ...vec3List[i + 2] ] // UV faceVertexUvs = [...faceVertexUvs, ...t3, ...t1, ...t2] } } const geometry = new THREE.BufferGeometry() // 顶点三角面 geometry.setAttribute( 'position', new THREE.BufferAttribute(new Float32Array(faceList), 3) ) // UV面 geometry.setAttribute( 'uv', new THREE.BufferAttribute(new Float32Array(faceVertexUvs), 2) ) return geometry }
-
经过前面步骤,得到网格体如下
-
添加默认状态和选中状态下材质
initMaterial () { const { initial, hover } = this._conf.style // 顶部材质 this._mt = {} this._mt.initial = new THREE.MeshBasicMaterial({ color: initial.color, transparent: true, opacity: initial.opacity, side: THREE.DoubleSide, wireframe: true }) this._mt.hover = new THREE.MeshBasicMaterial({ color: hover.color, transparent: true, opacity: hover.opacity, side: THREE.DoubleSide }) }
-
添加拾取事件,对选中的网格体Mesh设置选中材质,并对外派发事件
// 处理拾取事件 onPicked ({ targets, event }) { let attrs = null if (targets.length > 0) { const cMesh = targets[0]?.object if (cMesh?.type == 'Mesh') { // 设置选中状态 this.setLastPick(cMesh) attrs = cMesh._attrs } else { // 移除选中状态 this.removeLastPick() } } else { this.removeLastPick() } /** * 外派模型拾取事件 * @event ModelLayer#pick * @type {object} * @property {Number} screenX 图层场景 * @property {Number} screenY 图层相机 * @property {Object} attrs 模型属性 */ this.handleEvent('pick', { screenX: event?.pixel?.x, screenY: event?.pixel?.y, attrs }) }
-
外部监听到拾取事件,调动浮层展示详情
/** * 建筑单体化图层 * @return {Promise<void>} */ async function initMonoBuilding() { const data = await fetchData('../static/mock/monobuildingexample1.geojson') const layer = new MonoBuildingLayer({ //... data }) layerManger.add(layer) layer.on('pick', (event) => { updateMarker(event) }) } // 更新浮标 function updateMarker(event) { const { screenX, screenY, attrs } = event if (attrs) { // 更新信息浮层 const { id, name, belong, bottomAltitude, extendAltitude } = attrs tip.style.left = screenX + 20 + 'px' tip.style.top = screenY + 10 + 'px' tip.innerHTML = ` <ul> <li>id: ${id}</li> <li>楼层: ${name}</li> <li>离地高度: ${bottomAltitude}米</li> <li>楼层高度: ${extendAltitude}米</li> <li>所属: ${belong}</li> </ul> ` tip.style.display = 'block' // 更新鼠标手势 container.classList.add('mouse_hover') } else { tip.style.display = 'none' container.classList.remove('mouse_hover') } }
-
最终得到的交互效果如下
-
把3DTiles图层和点标记图层加上叠加显示,得到本示例最终效果
待拓展功能
-
对建筑模型单体的进一步细化
楼层功能还可以细化到每个楼层中各个户型,也许每个楼层都有独特的户型分布图,这个应该结合内部的墙体轮廓一起展示,选个弹窗在子内容页进行下一步操作,还是直在当前场景下钻到楼层内部?具体交互流程我还没想好。
-
如何处理异体模型
目前的方案仅针对规规矩矩的立方体建筑楼栋,而对于鸟巢、大裤衩、小蛮腰之类的异形地标性建筑,每个楼层的轮廓可能都是不一样的,因此在数据和代码方面仍需再做改进。