在高德地图实现可定制立体区块

2,438 阅读6分钟

介绍

最近圈子里有好几个朋友在问“带厚度的地理区块如何实现?如何给立体的区块添加边界线样式?” 高德地图本身提供了Loca.PolygonLayer立体面API,但翻阅开发文档后发现可配置的样式有限,为满足高度定制的需求,还是尝试实现了一下。

需求说明

基于GIS数据创建看高度定制化的立体区块:

1.传入标准的GeoJSON数据即可自动生成立体区块;

2.支持创建一个到多个多边形区块,每个区块相互独立,可自由定制高度;

3.支持可自由配置材质和颜色,比如地形、冰块、金属等;

4.支持可配置的边界线,可以调整线宽、颜色、材质、动态效果。

Honeycam_2022-12-27_18-05-30.gif

实现思路

1.先分析单个立体区块是怎么实现的,只要实现我们能够看得到的部分,那么把模型拆成顶部平面、侧面墙体、顶部边界线3大块分别实现即可。

Dingtalk_20221227172145.jpg

2.顶部平面的实现,先用坐标数据绘制一个形状THREE.Shape,再使用Shape生成多边形平面THREE.ShapeGeometry,如下图可以看到这是一个被三角剖分的平面,三角面是模型的最小面单位。

Untitled 1.png

3.接着给面添加材质THREE.MeshPhongMaterial ,选择这种材质的原因是可支持光照后光泽和高光效果,也支持普通纹理、法线纹理、凹凸纹理,方便做出较多中质感效果。

Untitled 2.png

4.绘制侧边墙体的实现方案在之前的文章中讲过了,这里就不赘述,有兴趣看看文末链接《在高德地图中进行THREE开发-边界墙图层》。

Untitled 3.png

5.绘制顶部边界线这块遇到点麻烦,原本使用了THREE.Line做绘制,这个Line本身是没有宽度的,无论如何设置width,最终展示只有1像素的线。事实上我们想要的不是线,而是平行于地面的可调节宽度的带状几何体,这里用到了第三方THREE类 meshline

Untitled 4.png

6.基本模型创建完毕,给顶部的边界线配置图片材质,通过调整材质的offset.x值创造动画效果。

Untitled.png

代码实现

1.提前准备好GeoJSON数据,文末提供了链接。

//数据示例
{
  "features": [
    {
      "type": "Feature",
      "properties": {
        "name": "广东省",
        "height": 1e5,
        "center": [
          113.47219628177336,
          22.80488376361114
        ]
      },
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [
              109.77970254852386,
              21.398842621700037
            ],
            [
              109.77971427545347,
              21.61849239587771
            ],
            [
              109.86746375606249,
              21.662234196126587
            ]...
          ]
        ]
      }
    }
  ]
}

2.并使用高德地图提供的map.customCoords转成当前Three.js直角坐标系上的坐标数据

/**
 * @description 转换地理坐标数据为three坐标数据
 */
initData (geoJSON) {
  const { features } = geoJSON
  features.forEach(item => {
    item.geometry.coordinates.forEach(v => {
      const target = v[0][0] instanceof Array ? v[0] : v
      const path = this.customCoords.lngLatsToCoords(target)
      this._paths.push(path) //缓存起来,供多处使用
    })
  })
}

3.绘制顶部平面

/**
 *@description绘制单个区块
 *@param{Array} path区块边界数据
 *@param{Object} option配置项
 *@param{Number} option.height区块高度
 *@param{String} option.name区块名称
*/
drawOneArea (path, { height, name }) {
  const { scene } = this
  const shape = new THREE.Shape()
  path.forEach(([x, y], index) => {
    if (index === 0) {
      shape.moveTo(x, y)
    } else {
      shape.lineTo(x, y)
    }
  })

  // 顶部面
  const geometry = new THREE.ShapeGeometry(shape)

  // 使用自定义材质,创建模型对象
  const mesh = new THREE.Mesh(geometry, this.getMaterial())
  // 调整模型海拔高度
  mesh.position.z = height || 0
  // 加入到场景
  scene.add(mesh)
}

4.绘制侧面

/**
   * @description 创建单个区块侧面
   * @param {Array} path 区块边界数据
   * @param {Object} option 配置项
   * @param {Number} option.height 区块高度
   * @param {String} option.name 区块名称
   */
  drawSide (path, { height = 0, name }) {
    if (height <= 0) {
      return
    }
    const arr = path
    // 保持闭合路线
    if (arr[0].toString() !== arr[arr.length - 1].toString()) {
      arr.push(arr[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 < arr.length; i++) {
      const [x1, y1] = arr[i]
      vec3List.push([x1, y1, 0])
      vec3List.push([x1, y1, height])
    }

    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))

    const material = new THREE.MeshBasicMaterial({
      // color: 'rgb(5,38,56)',
      side: THREE.DoubleSide,
      transparent: true,
      depthWrite: true
      // needsUpdate: true,
    })

    if (this._conf.sideMap) {
      const texture = new THREE.TextureLoader().load(this._conf.sideMap, () => {}, null, () => {
        console.error('sideMap load error')
      })
      texture.wrapS = THREE.RepeatWrapping
      texture.wrapT = THREE.RepeatWrapping
      texture.offset.set(0, 1)
      material.map = texture
    }
    const sideMesh = new THREE.Mesh(geometry, material)
    this.scene.add(sideMesh)
  }

5.绘制顶部边界线

/**
*@description创建单个区块的高亮边界线
*@param{Array} path区块边界数据
*@param{Object} option配置项
*@param{Number} option.height区块高度
*@param{String} option.name区块名称
*/
drawBorder (path, { height, name }) {
  const points = path.map(([x, y]) => {
    return new THREE.Vector3(x, y, height * 1.01)
  })

  const line = new MeshLineGeometry()
  line.setPoints(points)
  const mesh = new THREE.Mesh(line, this.getBorderMaterial())
  this.scene.add(mesh)
}

// 获取边界线材质
getBorderMaterial () {
  if (this._borderMaterial == null) {
    let texture

    const { borderWidth, borderColor, borderMap } = this._conf

    // texture.repeat.set(0.1, 0.1)

    const material = new MeshLineMaterial({
      lineWidth: borderWidth,
      sizeAttenuation: 1,
      useMap: borderMap ? 1 : 0,
      opacity: 1,
      transparent: false
    })

    if (borderMap) {
			// 有材质,则使用材质
      texture = new THREE.TextureLoader().load(this._conf.borderMap, () => {}, null, () => {
        console.error('borderMap load error')
      })
      texture.wrapS = THREE.RepeatWrapping
      texture.wrapT = THREE.RepeatWrapping

      material.map = texture
    } else {
			// 无材质,直接填色
      material.color = new THREE.Color(borderColor)
    }

    this._borderMaterial = material
  }
  return this._borderMaterial
}

6.逐帧更新边界线材质的offset.x ,实现动画效果

/**
   * @private
   * @description 逐帧动画更新内容
   */
  update () {  
    if (this._borderMaterial) {
      this._borderMaterial.uniforms.offset.value.x += 0.005
    }
  }

7.做组件封装,增加一些配置项,这个根据个人需求而定。

/**
   * 创建一个实例
   * @param  {Object} config
   * @param  {GeoJSON}  config.data 显示区域的坐标集
   * @param  {Number} config.opacity  图层透明度,支持(0,1)浮点数,默认值1.0
   * @param  {Number} config.altitude  图层海拔高度,默认值0.0
   * @param {String} config.topMap 顶部纹理路径,默认值'./static/texture/rock1.jpg'
   * @param {String} config.topMapRepeat 顶部纹理平铺UV配置,默认值 [0.0001, 0.0001]
   * @param {String} config.topMapNormal 顶部法向量纹理路径,默认值'./static/texture/rock1-normal.jpg'
   * @param {String} config.topColor 顶部颜色,可以与topMap叠加,默认值#0d8ce7
   * @param {String} config.sideMap 侧面纹理路径,默认值'./static/texture/texture_cake_1.png'
   * @param {String} config.borderColor 描边纹理颜色,与borderMap互斥,默认值 '#10ecda'
   * @param {String} config.borderWidth 描边宽度,默认值300
   * @param {String} config.borderMap 描边纹理路径,默认值 undefined
   * @param {Boolean} config.animate 是否支持边界线动画,指定了borderMap才会生效,默认值 false
   */
  constructor (config) {
    const conf = {
      data: null,
      opacity: 1.0,
      altitude: 0.0,
      animate: false,
      speed: 2,
      topMap: './static/texture/rock1.jpg',
      topMapNormal: './static/texture/rock1-normal.jpg',
      topColor: '#0d8ce7',
      topMapRepeat: [0.0001, 0.0001],
      sideMap: './static/texture/texture_cake_1.png',
      borderColor: '#10ecda',
      borderWidth: 300,
      borderMap: undefined,
      // borderMap: './static/texture/cake_border1.png',
      ...config
    }...

还能做哪些优化

1.活学活用,立体区块和建筑高度模型是同一个实现原理。

立体区块的本质其实还是1个简化的挤压缓冲几何体(ExtrudeGeometry),保留了顶部和侧面。只要有场景,它可以是数据区块、行政区块,也可以是建筑高度模型,因此我们只要保留同样的建模方法,再更换顶部和侧边的材质就可以得到一大块区域的配角建筑白模(样子还过得去,能够充实场景且可视性不会受高德本身地图缩放级别限制)

Dingtalk_20221227170515.jpg

2.生成真实地形

其实最终希望实现的是这样的,顶部材质能够与真实地形高度一致,像一块蛋糕(CakeLayer),有地形高低起伏的外观,有山有水,而不仅仅是法线材质做出来的凹凸效果。

Honeycam_2022-12-27_18-11-05.gif

彩蛋

针对“生成真实地形”这个需求我还专门去问了GPT-3,它给我的答复是这样的:

Untitled 5.png

初次看到这个答案的时候我是嗤之以鼻的,根据chat-GPT总是一本正经胡说八道的特性,我觉得它肯定是编造了不存在的类忽悠我。接着我去搜了关键词“THREE.TerrainLoader”,居然真的在github上找到了相关的代码和技术文章,算是提供了思路吧。给AI小助手点赞,了不起了不起。 github.com/sermonis/th…

参考链接

在高德地图中进行THREE开发-边界墙图层

juejin.cn/post/711976…

THREE.Meshline

github.com/pmndrs/mesh…

中国geojson数据

www.geojson.cn/