在三维地理信息系统开发中,地形高度数据就像建筑物的地基一样关键。去年参与某智慧城市项目时,我们需要在虚拟环境中精确放置5G基站模型,当时就深刻体会到准确获取地形高程的重要性——误差超过2米就会导致后续的基站信号覆盖分析完全失真。
Cesium作为当前最强大的Web三维地球引擎,其地形服务能提供从全球范围到厘米级精度的多种高程数据源。不同于传统GIS软件需要本地处理大量DEM数据,Cesium通过流式加载实现了在浏览器中实时查询任意位置的海拔高度,这对需要动态计算物体贴地位置的场景简直是革命性的突破。
典型应用场景包括:
Cesium的地形引擎就像个智能拼图高手。当镜头靠近某区域时,它会自动请求该位置对应精度级别的地形瓦片(Terrain Tile)。每个瓦片实质上是包含256×256个高程点的网格数据,采用quantized-mesh格式进行高效压缩传输。
高程数据的存储方式很有意思——它并非直接记录绝对海拔,而是存储相对于瓦片基准面的相对高度。这种设计使得单个瓦片文件大小能控制在100KB以内,全球地形数据量从TB级压缩到GB级。当我们需要获取具体坐标的高度时,Cesium会执行以下计算链:
javascript复制// 地形服务初始化示例
const viewer = new Cesium.Viewer('cesiumContainer', {
terrainProvider: Cesium.createWorldTerrain({
requestWaterMask: true, // 包含水域数据
requestVertexNormals: true // 包含法线数据
})
});
常见地形源性能对比表:
| 数据源 | 最高精度 | 覆盖范围 | 更新频率 | 免费额度 |
|---|---|---|---|---|
| Cesium World Terrain | 1m | 全球 | 年更 | 10万次/月 |
| Google Elevation API | 1m | 全球 | 实时 | $5/千次 |
| Mapbox Terrain | 5m | 全球 | 季更 | 5万次/月 |
| 本地DEM | 自定义 | 自定义 | 手动 | 无限制 |
提示:商业项目若需要高频次查询,建议使用Cesium ion的定制地形服务,其批量查询API的性价比显著高于Google等方案。
在无人机监控系统中,我们经常需要实时获取鼠标位置下方的地形高度。这种场景最适合使用sampleHeight方法:
javascript复制viewer.screenSpaceEventHandler.setInputAction((movement) => {
const ray = viewer.camera.getPickRay(movement.endPosition);
const position = viewer.scene.globe.pick(ray, viewer.scene);
if (Cesium.defined(position)) {
const height = viewer.scene.globe.getHeight(
Cesium.Cartographic.fromCartesian(position)
);
console.log(`当前海拔:${height.toFixed(2)}米`);
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
这个方法有三个技术要点:
当需要计算一条路径的坡度时,应该使用sampleTerrain方法进行批量查询。我们在某山地自行车路线规划项目中这样实现:
javascript复制const positions = [
Cesium.Cartographic.fromDegrees(116.3, 39.9),
Cesium.Cartographic.fromDegrees(116.305, 39.905)
];
Cesium.sampleTerrain(
viewer.terrainProvider,
11, // LOD级别
positions
).then((updatedPositions) => {
updatedPositions.forEach(pos => {
console.log(`经度:${pos.longitude}, 纬度:${pos.latitude}, 高度:${pos.height}`);
});
});
关键参数说明:
对于需要地形顶点数据的专业应用(如洪水淹没分析),可以通过terrainProvider获取原始网格:
javascript复制const tile = await viewer.terrainProvider.requestTileGeometry(
10, // x
20, // y
5, // level
true // 是否包含法线
);
const heights = tile.quantizedVertices.map(v => v.height);
const minHeight = Math.min(...heights);
const maxHeight = Math.max(...heights);
这种方法能获取到原始高程数组,但要注意:
在特殊情况下(如自定义着色器中),可以通过深度纹理反算高度:
javascript复制viewer.scene.postRender.addEventListener(() => {
const depthTexture = viewer.scene.context.depthTexture;
// 在着色器中通过深度值重建世界坐标
});
这种方法虽然灵活,但存在两个局限:
在车辆导航系统中,我们发现无节制的高度查询会导致性能断崖式下降。通过测试总结出这些经验值:
| 场景 | 推荐频率 | 采样半径 | 适用方法 |
|---|---|---|---|
| 鼠标悬停 | 60Hz | 1px | screenSpaceEventHandler |
| 物体贴地 | 10Hz | - | sampleTerrainMostDetailed |
| 路径规划 | 1Hz | 50m | sampleTerrain |
| 区域分析 | 单次 | 1km | requestTileGeometry |
特别要注意的是,在移动端应该将采样频率降低到桌面端的1/3,我们通过这个调整使某农业无人机的平板控制端帧率从15fps提升到45fps。
地形精度选择就像相机对焦——不是越高越好。经过实测不同LOD级别的性能影响:
| LOD | 误差范围 | 查询耗时 | 内存占用 |
|---|---|---|---|
| 8 | ±15m | 2ms | 5MB |
| 11 | ±3m | 8ms | 20MB |
| 14 | ±0.5m | 35ms | 80MB |
| 18 | ±0.01m | 120ms | 300MB |
建议采用动态LOD策略:
问题1:返回高度为undefined
问题2:高度值偏移几百米
问题3:性能突然下降
当使用机载LiDAR扫描的1米精度DEM时,需要创建CustomTerrainProvider:
javascript复制class MyTerrainProvider extends Cesium.TerrainProvider {
constructor(url) {
this._tiles = new Map();
this._error = undefined;
}
requestTileGeometry(x, y, level) {
const key = `${x}-${y}-${level}`;
if (this._tiles.has(key)) {
return this._tiles.get(key);
}
return Cesium.Resource.fetchJson(`terrain/${key}.json`)
.then(data => {
const mesh = new Cesium.QuantizedMeshTerrainData({
// 填充顶点、索引等数据
});
this._tiles.set(key, mesh);
return mesh;
});
}
}
关键实现要点:
在模拟工程施工时,我们实现了实时挖填方效果:
javascript复制const modifyHeight = (position, radius, deltaHeight) => {
const carto = Cesium.Cartographic.fromCartesian(position);
const tiles = getAffectedTiles(carto, radius);
tiles.forEach(tile => {
const heights = tile.getHeights();
// 应用高斯衰减公式修改高度
heights.forEach((h, i) => {
const dist = calculateDistance(i);
heights[i] = h + deltaHeight * Math.exp(-dist*dist/(2*radius*radius));
});
tile.updateHeights(heights);
});
};
这个方案的核心是:
军事仿真中的关键功能是判断两点间是否可视:
javascript复制function checkLineOfSight(start, end) {
const positions = [];
const steps = 100;
for (let i = 0; i <= steps; i++) {
const t = i / steps;
positions.push(Cesium.Cartesian3.lerp(start, end, t, new Cesium.Cartesian3()));
}
return Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, positions)
.then(samples => {
for (let i = 1; i < samples.length - 1; i++) {
const visible = isVisible(start, end, samples[i]);
if (!visible) return false;
}
return true;
});
}
算法优化技巧:
通过自定义材质实现实时坡度渲染:
javascript复制viewer.scene.globe.material = new Cesium.Material({
fabric: {
type: 'Slope',
uniforms: {
steepColor: new Cesium.Color(1.0, 0.0, 0.0),
gentleColor: new Cesium.Color(0.0, 1.0, 0.0),
threshold: 30.0
},
source: `
uniform vec3 steepColor;
uniform vec3 gentleColor;
uniform float threshold;
float getSlopeAngle(vec3 normal) {
return degrees(acos(normal.z));
}
void materialMain(vec3 normal, out vec3 color) {
float angle = getSlopeAngle(normal);
float t = smoothstep(0.0, threshold, angle);
color = mix(gentleColor, steepColor, t);
}
`
}
});
这个方案比CPU计算方案快20倍以上,特别适合大范围地形分析。