在高德地图上实现建筑模型动态单体化

4,016 阅读9分钟

前言

前段时间系统性地学习了国内知名GIS平台的三维应用解决方案,受益匪浅,准备分几期对所学内容进行整理归纳和分享,今天来讲讲城市3D模型的处理方案。

城市3D模型图层在Web GIS平台中能够对地面物体进行单独选中、查询、空间分析等操作,是属于比较普遍的功能需求。这样的图层一般不会只有单一的数据来源,而是个集成了倾斜投影、BIM建模、动态参数建模的混合型图层,并且需要对模型进行适当的处理优化以提升其性能,因此在GIS平台不会选择支持单独地选中地图,我们需要对模型进行拆分——即单体化处理。

对建筑模型单体化处理通常有三种方案,各有其优点和适用场景,对比如下:

方案实现原理优势缺陷
切割单体化将三维模型与二维面进行切割操作成为单独可操作对象能够实现非常精细的单体化效果,适用于精细度要求高的场景数据处理量大,对计算机性能要求较高
ID 单体化预先为每个三维模型对象和二维矢量数据分配一个可关联的 ID数据处理相对简单,不需要进行复杂的几何运算,适用于数据量相对较小、模型结构相对简单的场景可能会出现 ID 管理困难的情况,且对于模型的几何形状变化适应性较差
动态单体化实时判断鼠标点击或选择操作的位置与三维模型的关系,动态地将选中的部分从整体模型中分离出来进行单独操作无需预先处理数据,可根据用户的交互实时进行单体化操作,适用于需要快速响应用户交互的场景对计算机的图形处理能力和性能要求较高

在这些方案中,动态单体化无需对模型进行预处理,依数据而变化使用较为灵活,接下来我们通过一个简单的例子来演示该方案的整体实现过程,源代码在这里供下载交流。

Honeycam_2024-08-18_16-31-38.gif

需求分析

假设我们拿到一个城市行政区域内的三维建筑数据,对里面该区域里面的几栋关键型建筑进行操作管理,我们需要把建筑模型放置到对应的地图位置上,可宏观地浏览模型;支持通过鼠标选中建筑的楼层,查看当前楼层的相关数据;楼层数据支持动态变更,且可以进行结构性存储。因此我们得到以下功能需求:

  1. 在Web地图上建立3D区域模型图层
  2. 根据当前光标位置动态高亮楼层,并展示楼层基本信息
  3. 建筑单体化数据为通用geoJSON格式,方便直接转换为csv或导入数据库

技术栈说明

工具名称版本用途
高德地图 JSAPI2.0为GIS平台提供基础底图和服务
three.js0.157主流webGL引擎之一,负责实现展示层面的功能
QGIS3.32.3GIS数据处理工具,用于处理本文的矢量化数据
cesiumlab3.1.11三维数据处理工具集,用于将模型转换为互联网可用的3DTiles
blender3.6模型处理工具,用于对BIM模型进行最简单的预处理

实现步骤

制作3DTiles

城市级的三维模型通常以无人机倾斜投影获取到的数据最为快捷,数据格式为OSGB且体积巨大不利于分享,由于手头没有合适的倾斜投影数据,我们以一个小型的BIM模型为例进行三维瓦片化处理也是一样的。

  1. 模型预处理。从sketchfab寻找到一个合适的建筑模型,下载其FBX格式并导入到模型处理工具(C4D、blender等)进行简单的预处理,调整模型的大小、重置坐标轴原点的位置到模型的几何中心,然后导出带材质的FBX模型备用,这里blender如何带材质地导出模型有一些技巧。

    image.png

  2. 启动cesiumlab,点击“通用模型切片”选项,选择预处理好的模型,指定它的地理位置(ENU: 维度,经度),点击确认

    image 1.png

  3. 在最后的“数据存储”设置原始坐标为打开、存储类型为散列(最终输出多个文件)、输出路径,提交处理等待3DTiles生成

    image 2.png

  4. 生成过程结束后我们来到分发服务选项,点击chrome的图标就能够进入3DTiles的预览了,注意看路径这一列,这里面包含了入口文件tileset.json的两个路径(文件存储目录和静态资源服务地址),后面开发中我们会用到它。

    image 3.png

  5. 至此模型准备完毕,我们可以把输出出的3Tiles目录放到开发i工程中,也可以单独部署为静态资源服务,保证tileset.json可访问即可。

    image 4.png

  6. 开发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)
    }
    
  7. 这一阶段实现的效果如下

    Honeycam_2024-08-18_17-45-29.gif

创建单体化数据

  1. 使用QGIS处理矢量数据,绘制建筑模型轮廓的矢量面。由于本示例是虚拟的,我们需要自己创建矢量数据,把上一个步骤完成的内容高空垂直截图导入到QGIS上配准位置,作为描绘的参考图。

    image 5.png

  2. 创建形状文件图层,进入编辑模式绘制建筑轮廓

    image 6.png

  3. 选择图层右键打开属性表,开始编辑每个建筑的基础数据,导出为monobuildingexample1.geojson

    image 7.png

  4. 对关键建筑“商业办公楼A”和“商业办公楼B”的楼层数据进行进一步编辑(bottomAltitude为每层楼的离地高度,extendAltitude为楼层高度),这块数据与GIS无关,我直接用wps图表去做了,完成后将csv文件转换成为json格式,然后与monobuildingexample1.geojson做一个组合,得到最终的geoJSON数据。

    image 8.png

开发动态单体化图层

底座和数据准备好,终于可以进行动态单体化图层开发了,实现原理其实非常简单,根据上一步获得的建筑矢量和楼层高度数据,我们就可以在于模型匹配的地理位置上创建若干个“罩住”楼栋模型的盒状网格体,并监听网格体的鼠标拾取状态,即可实现楼层单体化交互。

image 9.png

  1. 我们的数据来自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
    }
    
  2. 经过前面步骤,得到网格体如下

    Honeycam_2024-08-18_18-24-35.gif

  3. 添加默认状态和选中状态下材质

    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
      })
    }
    
  4. 添加拾取事件,对选中的网格体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
      })
    }
    
  5. 外部监听到拾取事件,调动浮层展示详情

    /**
     * 建筑单体化图层
     * @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')
      }
    }
    
  6. 最终得到的交互效果如下

    Honeycam_2024-08-18_18-56-30.gif

  7. 把3DTiles图层和点标记图层加上叠加显示,得到本示例最终效果

    Honeycam_2024-08-18_19-03-00.gif

待拓展功能

  1. 对建筑模型单体的进一步细化

    楼层功能还可以细化到每个楼层中各个户型,也许每个楼层都有独特的户型分布图,这个应该结合内部的墙体轮廓一起展示,选个弹窗在子内容页进行下一步操作,还是直在当前场景下钻到楼层内部?具体交互流程我还没想好。

  2. 如何处理异体模型

    目前的方案仅针对规规矩矩的立方体建筑楼栋,而对于鸟巢、大裤衩、小蛮腰之类的异形地标性建筑,每个楼层的轮廓可能都是不一样的,因此在数据和代码方面仍需再做改进。

    image 10.png

本示例使用到的高德JSAPI

3D自定义图层AMap.GLCustomLayer

AMap.Map地图对象类

点标记: 用于在地图上添加点状地图要素

空间数据计算的函数库 GeometryUtil

相关工具链接

Sketchfab上免费下载的小区模型

使用blender导出带材质的FBX文件

在线将cvs文件转换为JSON