Cesium中实现根据最高地形瓦片生成高度图

高度图

对于 GIS 相关专业的同学来说,高度图应该是一个很熟悉的概念,它最常见的表现形式是 DEM(数字高程模型)。

高度图通常是一张纹理,纹理的每个像素存储了对应位置的高度信息

在 GIS 中常用于表示地形,Cesium 如果使用 ArcGIS 发布的地形服务的话,实际上就是使用的高度图来展现地形的。

除了用了生成地形,高度图还可以用于各种科学领域的模拟,比如洪水模拟、地形侵蚀、评估土地的适宜性、进行物理模拟计算等。

高度图

根据地形瓦片获取高度图

在 Cesium 中,我们制作流体模拟、地形侵蚀、物理碰撞等功能,基本都是通过高度图来进行计算模拟的,可以说高度生成的质量会直接影响到模拟的效果。

我在这里提供一个通过地形瓦片生成高度图的思路,供大家参考学习。

由于高度图的表现形式是图片,因此生成高度图的范围一定是一个矩形。在 Cesium 中,我们可以通过 Cesium.Rectangle 来表示这个范围

这个生成高度图的流程主要分为以下几个步骤。

  1. 创建一个 Cesium.Rectangle 来表示需要生成高度图的范围
  2. 获取与 Cesium.Rectangle 相交的最高等级的瓦片
  3. 提取地形瓦片的顶点和索引信息
  4. 创建地形瓦片转高度图的着色器计算模块(每个地形瓦片对应一个计算模块)
  5. 将所有着色器计算模块输出内容绑定到同一张纹理上
  6. 执行所有着色器计算模块,获取高度图

获取与 Cesium.Rectangle 相交的最高等级的瓦片

Cesium 官方提供了 computeBestAvailableLevelOverRectangle 方法,允许我们获取指定四至范围内地形可用的最高级别。

获取具体级别后,就需要遍历地形的四叉树结构,获取所有与给定四至相交的瓦片行列号(指定等级下的)。

最后根据行列号发起对地形文件的请求,即可获取与 Cesium.Rectangle 相交的最高等级的瓦片。

提取地形瓦片的顶点和索引信息

请求地形文件回来之后,需要对其进行解析,获取当前瓦片对应的顶点和索引信息。

具体实现步骤可以参考 Cesium 源码中对应地形类型的 TerrainData 类。

创建地形瓦片转高度图的着色器计算模块

获取顶点和索引之后,怎么将这些信息转换成高度图信息呢?

我们其实可以换个思路,创建一个与给定四至范围相同的正交相机,从上往下看并渲染所有的地形瓦片,最终渲染出的内容就是我们所需的高度图。

如图所示,红色部分为我们选中的四至范围,黑色部分为与四至相交的最高等级瓦片,绿色部分为我们虚拟出来的正交相机。

提取高度图的思路

但是,如果经历完整的 MVP 变换来进行渲染,不但很麻烦,而且精度会丢失很多,那么有没有什么更高效的方法呢?

如果了解渲染流程,就会清楚,模型经过 MVP 变换后,最终能够渲染在屏幕上的坐标值被限制在 ndc 空间内。

在 ndc 空间下,x,y 会规范到 [-1, 1] 之间,z规范到 [0, 1] 之间。

因此我们可以提前对地形的顶点进行转换

转换方法如下

将顶点转换为经纬度高程的形式(cartographic)。根据四至将经纬度映射到 [ -1, 1 ] 作为 XY 值,将高程作为 Z 值

1
2
3
4
5
const x = (cartographic.longitude - rectangle.west) / (rectangle.east - rectangle.west) * 2.0 - 1.0;

const y = (cartographic.latitude - rectangle.south) / (rectangle.north - rectangle.south) * 2.0 - 1.0;

const z = cartographic.height;

经过转换后的地形坐标就天然实现了平铺在屏幕上,并且屏幕显示的内容就是四至范围对应的内容

这时转换高度图的着色器就很好写了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// vertexShaderSource
precision highp float;
in vec4 position;
out float height;
void main () {
vec4 pos = vec4(position.xy, 1.0, 1.0);
height = position.z;
gl_Position = pos;
}

// fragmentShaderSource
precision highp float;
in float height;
void main () {
out_FragColor = vec4(vec3(height), 1.0);
}

将所有着色器计算模块输出内容绑定到同一张纹理上

由于每块地形瓦片对应一个着色器计算模块,每个计算模块只能绘制出高度图的一部分,若需要绘制出完整的高度图,则需要将所有计算模块输出的纹理绑定在同一个 framebuffer 上

值得注意的是,这里不能使用 ComputeCommand 来进行计算输出,因为 ComputeCommand 每次执行都会清空当前的纹理内容。导致所有地形瓦片的计算模块执行完之后,framebuffer 上只剩下最后执行结果。

解决方法是编写更底层的 DrawCommand 来进行计算,避免 framebuffer 在着色器执行前被清空。

课后拓展

如果你实现了上面的计算方法,会发现,由于 Cesium 的地形瓦片之间并不是严丝合缝的,因此两个相邻的地形瓦片之前可能会有一行或者一列像素是冲突的或者没有高程值的。

高度图上的缝隙

在 Cesium 渲染地形的时候,为什么我们没有看到瓦片之间的缝呢?这是因为,Cesium在渲染地形时,给每一块地形的边缘增加了一个裙边,通过两个相邻的地形瓦片裙边来遮住这个缝隙。

执行代码 viewer.scene.globe.showSkirts = false; 将相机拉进地形,应该就可以看到这个缝隙了

地形之间的缝隙

解决方法当然也有,对于冲突的问题,我们可以增加一个深度信息。对于没有高程值的问题,我们可以对高度图再进行一次处理,将没有高程值的地方通过周围像素进行插值补齐。

修复效果如下:

修复高度图上的缝隙

获取效果展示


Cesium中实现根据最高地形瓦片生成高度图
https://www.liaomz.top/2025/02/19/cesium-zhong-shi-xian-gen-ju-zui-gao-di-xing-wa-pian-sheng-cheng-gao-du-tu/
作者
发布于
2025年2月19日
许可协议