在高德地图中实现3DTiles

3,795 阅读3分钟

前言

有一天领导跑过来跟我说,现在都是数字孪生元宇宙概念满天飞的时代了,咱们的产品要是不搞点搭边的功能好像说不过去啊,这样,我已经联系外面合作公司把我们办公楼社区整个用无人机拍下来建模了,你这两天把这个3D模型整到我们产品的地图上,然后做一些blingbling的效果。

接到这个任务的时候我内心其实是高兴的,看够了高德地图常年素面朝天的建筑白模,终于可以搞点新花样,还有现成的数据、现成的数据采集团队可以配合,岂不是天时地利人和。这波可以冲。

需求说明

  1. 将指定地理区域的无人机倾斜投影建筑模型置入高德卫星地图对应的地理位置上,实现快速加载和多级别细节加载,即3DTiles
  2. 3DTiles支持与现有其他GL图层兼容,即可以在其上叠加各种动态图层
  3. 3DTiles支持鼠标交互操作,对鼠标事件产生响应,比如鼠标悬浮到某个楼栋模型会产生高亮效果

Honeycam 2022-09-15 11-31-51.gif

名词解释

3D Tiles:是一种开放的三维空间数据标准,其设计目的主要是为了提升大的三维场景中模型的加载和渲染速度。3DTiles的加载策略为LOD。

LOD: Level of Details,简称为多细节层次。LOD技术根据模型的节点在显示环境中所处的位置(Screen Size)和重要度,来决定物体渲染的资源分配,降低非重要物体的面数和细节数,从而获得高效率的渲染计算。

实现思路

高德地图JS API 2.0官方提供了一个3D Tiles图层示例,代码量少看起来很简单,那么优先尝试这种方式吧。

AMap[”3DTIilesLayer”]高德示例

developer.amap.com/demo/jsapi-…

方法1:使用AMap[”3DTIilesLayer”]组件

1.获取OSGB模型,由数据采集团队提供,主要数据文件为.osgb格式,可使用DasViewer等查看器直接浏览模型;

2.使用cesiumlab转换模型为3dtiles格式,转换后主要数据文件为.b3dm和.json格式,转换步骤B站上看视频教程花十分钟就学会了;

3.完成前面步骤后可以得到名为tileset.json的入口文件和一堆文件夹,现在本地测试一下接入

<div id="container" class="map" tabindex="0"></div>
<script type="text/javascript">
var map = new AMap.Map("container", {
		viewMode: '3D',
	  center: [lng,lat],
})
var tiles = new AMap['3DTilesLayer']({
    map: map,
    url: '${filePath}/tileset.json' // 3DTiles入口文件地址
});
</script>

一切看上去很简单美好,燃鹅在折腾了两天反复试验步骤1、2后以失败告终。原因是我使用的工具,生成不了符合高德地图所需要的墨卡托坐标格式的文件,尝试着调整tileset.json文件中的坐标数据也无效。询问官方人员反馈没有工具可提供;互助答疑圈询问了尝试过该方法的大佬,他答复也是转换不了换了其他方案。

Dingtalk_20220915084030.jpg

后来我也被迫放弃了这个方案,因为以下几个致命问题:

1.需要通过js引入额外的旧版本three0.117和GLTFLoader插件,品尝过0.142性能的美好,没理由换回低版本将性能吧;

2.与GLCustomLayer图层不兼容,即不能叠加blingbling的动效图层;

3.没有文档,没法调试,即使我已经把图层接入了,我有没有办法知道模型到底定位到了地图上哪个角落。(此处偷偷@高德开发人员)

一系列打击就好像一瓢清水泼到脸上,让我逐渐清醒了一点,进而灵感爆发,忽然回想到之前貌似用three.js接入过3DTiles,既然高德提供了GLCustomLayer图层用来做three开发,那么可不可以将3DTiles放到GLCustomLayer图层上呢?瞬间感觉思路又打开。

微信截图_20220913113623.png

方法2:使用AMap.GLCustomLayer图层接入模型

地图接入示意图.jpg

1.获取OSGB模型,由数据采集团队提供,主要数据文件为.osgb格式,可使用DasViewer等查看器直接浏览模型;

2.使用cesiumlab转换模型为3dtile格式,注意将零点位置设置为(0,0,0),因为这种方式需要我们自己定位模型的原点位置了;

Untitled.png

3.安装'3d-tiles-renderer' ,该插件用于在three.js场景中展示3dTile,如图所示。实际操作过程中可能第一次出现的模型没有那么完美,可能会被翻转过,这是因为cesiumlab是以地球球体为参照物创建的模型,与我们最终需要的以平面为参照物的高德地图会有出入,没关系,调整一下入口文件tileset.json的transfrom翻转矩阵就行了。

Untitled (1).png

4.实例化AMap.GLCustomLayer,作为高德地图的一个图层,并调整一下图层的原点(0,0)位置,调用customCoords.lngLatsToCoords 会将参数数组的第一个成员作为图层的原点,因此我们传3Dtiles原点的高德坐标就可以实现模型定位。

5.将3DTiles用3d-tiles-renderer加载放到刚刚实例化的GLCustomLayer图层中。

Untitled (2).png

6.接入之前实现过的路灯模型图层,这里路灯的朝向是自动跟实际道路方向垂直的,用到了之前介绍过的算法,感兴趣的可以去看看以前的文章。 基于GIS实现路灯朝向自动校正 - 掘金

Dingtalk_20220915104830 (1).jpg

7.接入其他花里胡哨的动效图层,也是之前介绍过的,调整一下数据就拿来用了,真香。

Honeycam 2022-09-13 17-34-22.gif

8.实现鼠标交互,鼠标悬浮高亮对象,这些是three中Raycaster 拾取的方案,就不赘述了。后续楼栋的模型做单体化了,交互的对象可以更加细致。

Honeycam 2022-09-15 11-04-13.gif

代码实现

创建图层类,核心代码如下

import * as THREE from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
import { TilesRenderer } from '3d-tiles-renderer'
import Layer from './Layer'

/**
 *  倾斜摄影3DTile图层
 *  class TilesLayer
 *  container: 容器
 *  map: 地图
 *  tilesURL: 数据文件路径,入口文件一般为 tileset.json
 *  zooms: 显示区间 [5,14]
 */
class TilesLayer extends Layer {
  // 切片原点位置 [lng,lat]
  #position = null

  // 切片渲染器集
  #tilesRendererArr = []

  // 数据来源地址
  #tilesURL = ''

  // 上一个被选中对象
  #lastPick = null

  constructor ({
    container, // 容器
    map,
    position,
    zooms,
    tilesURL
  }) {
    super(arguments[0])
    this.#tilesURL = tilesURL
    this.getData(position)
  }

  // 实例化构造完成,init后,会先执行这里
  onReady () {
    // 加载图层内容
    this.loadTiles()
  }

  getData (position) {
    // 地理坐标转three坐标系,不管用不用arr,都需要转换一个非空数组
    // 否则customCoords没实例化api会报错
    const res = this.customCoords.lngLatsToCoords(position)
    this.#position = res
  }

	loadTiles () {
    // 借助3d-tiles-renderer实现模型加载,都是网上扒下来的示例
    const dracoLoader = new DRACOLoader()
    dracoLoader.setDecoderPath(`${basePath}/static/three/examples/js/libs/gltf/`)
    const loader = new GLTFLoader()
    loader.setDRACOLoader(dracoLoader)

    const tilesRenderer = new TilesRenderer(this.#tilesURL)
    tilesRenderer.manager.addHandler(/\.gltf$/, loader)

    tilesRenderer.setCamera(this.camera)
    tilesRenderer.setResolutionFromRenderer(this.camera, this.renderer)

	  // 模型得手,放到scene
    this.scene.add(tilesRenderer.group)
    this.#tilesRendererArr.push(tilesRenderer)
  }

  // 该方法会在requestAnimationFrame中执行
  update () {
    for (const tilesRenderer of this.#tilesRendererArr) {
      // 更新模型渲染器
      tilesRenderer.update()
    }
  }

}

超类Layer.js核心代码

import * as THREE from 'three'

class Layer {
  // 图层显示范围
  #zooms = [3, 22]

  // 默认支持动画
  isAnimate = true

  // 图层中心坐标
  #center = null

  // 射线,用于做物体拾取
  #raycaster = null

  // 支持鼠标交互
  #interactAble = false

  constructor (conf) {
    this.container = conf.container
    this.map = conf.map
    this.#interactAble = conf.interact || false
    this.customCoords = this.map.customCoords
    this.layer = null
    // 显示范围
    if (conf.zooms) {
      this.#zooms = conf.zooms
    }
    // 支持动画
    if (conf.animate !== undefined) {
      this.isAnimate = conf.animate
    }
    // three相关属性
    this.camera = null
    this.renderer = null
    this.scene = null
    // 事件监听字典
    this.eventMap = {}
    this.initLayer()
  }

  // 初始化图层
  initLayer () {
    return new Promise((resolve) => {
      this.layer = new AMap.GLCustomLayer({
        zIndex: 9999,
        visible: this.isInZooms(),
        init: (gl) => {
          this.initThree(gl)
          this.onReady() 
          this.#center = this.customCoords.getCenter()
          this.animate()
          resolve()
        },
        render: () => {
          const { scene, renderer, camera, customCoords } = this

          // 重新定位中心,这样才能使当前图层与Loca图层共存时显示正常
          if (this.#center) {
            customCoords.setCenter(this.#center)
          }

          const { near, far, fov, up, lookAt, position } = customCoords.getCameraParams()

          camera.near = near// 近平面
          camera.far = far // 远平面
          camera.fov = fov // 视野范围
          camera.position.set(...position)
          camera.up.set(...up)
          camera.lookAt(...lookAt)

          // 更新相机坐标系
          camera.updateProjectionMatrix()

          renderer.render(scene, camera)

          // 这里必须执行!重新设置 three 的 gl 上下文状态
          renderer.resetState()
        }
      })

      this.map.add(this.layer)
    })
  }

  animate () {
    if (this.update) {
      this.update()
    }
    if (this.map) {
      this.map.render()
    }
    requestAnimationFrame(() => {
      this.animate()
    })
  }

}

Tiles图层实例化

const layer = new TilesLayer({
    container: container.value,
    map: getMap(),
    position: [113.55015, 22.761426], // 这个参数决定了模型的位置
    zooms: [4, 22], // 在哪个地图缩放等级可见
    zoom: 19.48,
    interact: false,// 是否做鼠标交互
    tilesURL: '${basePath}/tileset.json' // 入口文件地址
  })

注意点

1.由于包含了各个缩放层级的模型和大量细节,3DTiles的文件体积非常大的,比如案例中的小区0.3平方公里,数据体积也达到1.05GB,因此建议3DTiels单独部署

2.为确保输出的3DTiles是可用的,强烈建议学会看入口文件tileset.json,相关的入口文章在这里玩转3D数据标准--3D Tiles

3.我们需要修改tileset.json中transform转置矩阵,最简单的做法是把它设为单位矩阵,让它待在坐标原点位置,不做任何变化。如果你实在想调整模型,我这边也提供了生成对应转置矩阵的方法

image.png

// 这是一个单位矩阵
const transform = [
  1, 0, 0, 0,
  0, 1, 0, 0,
  0, 0, 1, 0,
  0, 0, 0, 1
];

// 输入变换参数,获得一个转置矩阵
getTransformationMatrix(scaleX, scaleY, scaleZ, rotateX, rotateY, rotateZ, translateX, translateY, translateZ);

相关链接

1.AMap[”3DTIilesLayer”]高德示例

developer.amap.com/demo/jsapi-…

2.AMap.GLCustomLayer高德示例

developer.amap.com/demo/jsapi-…