Cesium中实现根据最高地形瓦片生成高度图
Cesium中实现根据最高地形瓦片生成高度图
在GIS领域,高度图是一个非常重要的概念,它通常以DEM(数字高程模型)的形式出现,用于表示地形的高低起伏。在Cesium中,通过高度图可以实现流体模拟、地形侵蚀、物理碰撞等功能。本文将详细介绍如何在Cesium中根据最高地形瓦片生成高度图。
高度图
对于GIS相关专业的同学来说,高度图应该是一个很熟悉的概念,它最常见的表现形式是DEM(数字高程模型)。
高度图通常是一张纹理,纹理的每个像素存储了对应位置的高度信息。
在GIS中常用于表示地形,Cesium如果使用ArcGIS发布的地形服务的话,实际上就是使用高度图来展现地形的。
除了用于生成地形,高度图还可以用于各种科学领域的模拟,比如洪水模拟、地形侵蚀、评估土地的适宜性、进行物理模拟计算等。
根据地形瓦片获取高度图
在Cesium中,我们制作流体模拟、地形侵蚀、物理碰撞等功能,基本都是通过高度图来进行计算模拟的,可以说高度生成的质量会直接影响到模拟的效果。
我在这里提供一个通过地形瓦片生成高度图的思路,供大家参考学习。
由于高度图的表现形式是图片,因此生成高度图的范围一定是一个矩形。在Cesium中,我们可以通过Cesium.Rectangle来表示这个范围。
这个生成高度图的流程主要分为以下几个步骤:
- 创建一个Cesium.Rectangle来表示需要生成高度图的范围。
- 获取与Cesium.Rectangle相交的最高等级的瓦片。
- 提取地形瓦片的顶点和索引信息。
- 创建地形瓦片转高度图的着色器计算模块(每个地形瓦片对应一个计算模块)。
- 将所有着色器计算模块输出内容绑定到同一张纹理上。
- 执行所有着色器计算模块,获取高度图。
获取与Cesium.Rectangle相交的最高等级的瓦片
Cesium官方提供了computeBestAvailableLevelOverRectangle
方法,允许我们获取指定四至范围内地形可用的最高级别。
获取具体级别后,就需要遍历地形的四叉树结构,获取所有与给定四至相交的瓦片行列号(指定等级下的)。
最后根据行列号发起对地形文件的请求,即可获取与Cesium.Rectangle相交的最高等级的瓦片。
提取地形瓦片的顶点和索引信息
请求地形文件回来之后,需要对其进行解析,获取当前瓦片对应的顶点和索引信息。
具体实现步骤可以参考Cesium源码中对应地形类型的TerrainData类。
创建地形瓦片转高度图的着色器计算模块
获取顶点和索引之后,怎么将这些信息转换成高度图信息呢?
我们其实可以换个思路,创建一个与给定四至范围相同的正交相机,从上往下看并渲染所有的地形瓦片,最终渲染出的内容就是我们所需的高度图。
如图所示,红色部分为我们选中的四至范围,黑色部分为与四至相交的最高等级瓦片,绿色部分为我们虚拟出来的正交相机。
但是,如果经历完整的MVP变换来进行渲染,不但很麻烦,而且精度会丢失很多,那么有没有什么更高效的方法呢?
如果了解渲染流程,就会清楚,模型经过MVP变换后,最终能够渲染在屏幕上的坐标值被限制在ndc空间内。
在ndc空间下,x,y会规范到[-1, 1]之间,z规范到[0, 1]之间。
因此我们可以提前对地形的顶点进行转换。
转换方法如下:
将顶点转换为经纬度高程的形式(cartographic)。根据四至将经纬度映射到[-1, 1]作为XY值,将高程作为Z值
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;
经过转换后的地形坐标就天然实现了平铺在屏幕上,并且屏幕显示的内容就是四至范围对应的内容。
这时转换高度图的着色器就很好写了。
// 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中根据最高等级地形瓦片生成高度图
原文链接
Cesium中实现根据最高地形瓦片生成高度图