在高德地图上实现山地地形
前言
在GIS应用中,有时候需要模拟展示一些地形效果,特别是山地区域。我们可以使用多种方式来创建山地地形模型,其中最常用的方法是使用数字高程模型(DEM),它是一种用于描述地形高度的数字模型。通过DEM,可以获取地形的高程和坡度等信息,并将其转换为三维地形模型,本文浅显地介绍了一种实现方法。
需求说明
- 使用高度图作为原始数据,在地图中生成对应的地形
- 地形区域为矩形,可指定中心、宽高、单位高度
- 支持更换地形的纹理,可以使用影像地图、等高线图、高度图等
名词解释
高度图:也称高程图(Heightmap)、高程模型或灰度图,是一种用于描述地形高度的二维图像,常用于游戏和虚拟现实应用中的地形建模。高度图通常是一个灰度图像,其中每个像素的灰度值表示该点的高度。较暗的像素通常表示较低的高度,而较亮的像素则表示较高的高度。通过将这些高度值映射到三维网格上,可以创建出具有自然地形的山脉、河流、峡谷等地貌特征的三维地形模型。
DEM(Digital Elevation Model,数字高程模型)数据是用于描述地形高度的数字模型,数据形式可以是文本格式ASCII Grid,图像格式GeoTIFF,二进制格式SDTS DEM
实现思路
1.创建一个平面几何体Plane,创建时配置好Plane的位置,尺寸,宽高方向的分段数。分段数决定了山地地形层最终的顶点数量,顶点越多图层越平滑,性能损耗也随着增加
2.使用高度图Heightmap作为原始数据,调整Plane上每个顶点的z轴高度,Heightmap上的像素点和Plane上的顶点有一定的映射关系,每个顶点对应Heightmap上具有相同位置关系的像素点P,P越接近白色,则顶点高度越高
3.贴纹理,将平面所在区域的卫星影像图(或其他任意图片)
4.贴法线图增加凹凸感,法线图可以使用纹理图直接在Photoshop生成
5.增加一些场景,比如卫星图层、风车发电器什么的,看起来比较像那么回事。
代码实现
- 加载高度图,处理DEM数据
// 加载图片
loadTerrainImg () {
return new Promise((resolve) => {
const img = new Image()
img.src = this._conf.terrainTexture
img.onload = () => {
this._terrainImg = img
resolve(img)
}
})
}
// 获取高度图中的高度属性转为Js对象
getTerrainData () {
const img = this._terrainImg
const { width, height } = img
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
// 将图片绘制到画布上
ctx.drawImage(img, 0, 0, width, height)
// 获取画布上的图像像素矩阵
const { data } = ctx.getImageData(0, 0, width, height)
const res = []
/**
* 数据获取规则:先宽后高
*/
for (let j = 0; j < height; j++) {
res.push([])
for (let i = 0; i < width; i++) {
const index = i + j * width
// 每个像素点信息为r、g、b、a,且r=g=b,取r的值即可
res[j].push(data[4 * index])
}
// 宽度加1,以覆盖所有顶点
res[j].push(res[j][res[j].length - 1])
}
// 高度加1,以覆盖所有顶点
res.push([...res[res.length - 1]])
// console.log(res)
return res
}
- 创建几何体Plane,并根据高度图数据调整Plane为地形几何体
createPlaneGeometry () {
// 创建几何面
const { width, height, widthSegments, heightSegments } = this._conf.style
const geometry = new THREE.PlaneGeometry(width, height, widthSegments, heightSegments)
// 获取高度图数据
const widthHeightArr = this.getTerrainData()
// 调整几何面上每个顶点的高度,高度值来自terrainData
for (let j = 0; j <= heightSegments; j++) {
for (let i = 0; i <= widthSegments; i++) {
const _i = Math.floor(i / widthSegments * this._terrainImg.width)
const _j = Math.floor(j / heightSegments * this._terrainImg.height)
geometry.attributes.position.array[(i + j * (widthSegments + 1)) * 3 + 2] = widthHeightArr[_i][_j] * this._conf.unitHeight
}
}
return geometry
}
- 贴纹理,创建模型,此时图层创建完毕
createModel () {
// 几何体
const geometry = this.createPlaneGeometry()
// 纹理材质
const texture = new THREE.TextureLoader().load(this._conf.mapTexture)
const normal = new THREE.TextureLoader().load(this._conf.normalTexture)
const material = new THREE.MeshStandardMaterial({
map: texture, //纹理贴图
normalMap: normal, // 法线贴图,用于模拟凹凸感
side: THREE.DoubleSide,
blending: THREE.NormalBlending
})
const plane = new THREE.Mesh(geometry, material)
this.scene.add(plane)
}
- 添加光照,这里只加了一个平行光
layer.on('complete', ({ scene }) => {
// 平行光
const directLight = new THREE.DirectionalLight('#ffffff', 0.6)
directLight.position.set(1, -1, 1)
scene.add(directLight)
})
注意点
高度图与地形面Plane顶点的对应关系,可以不是一对一的,但高度图和地形面的宽高比必须一致。比如我使用了一张宽高分别为501px的图片保存地形高度数据,那么就一共有501501=251001个高度信息,Plane在宽高方向上的分段数分别为20,那么一共有2121=441个顶点,每个顶点只要找到高度图在对应参考位置的高度就行了。
还可以做哪些改进
本文的方法适用于在地形叠加更多模型的场景。如果将地图可视范围放大到省份、国家级别的尺度,还要兼顾地图拖动、缩放、旋转时同步更新,如果还使用创建几何体的方式去实现的话其实是不方便的。
山地只要“看上去”有高低错落的感觉就行了,在着色器中完全可以模拟出来,不需要建立真正高度地形,那么我们完全给一个平面赋予这样一种自定义材质,将当前的DEM数据传入着色器模拟出山地效果,文末链接有具体的示例介绍。
高德地图Loca的3D热力图应该就是这样实现的。
相关链接
常用的gltf模型下载源
世界地形高度图
tangrams.github.io/heightmappe…
Three.js重建真实地形