Cesium 最高地形瓦片高度图实现说明
记录 Cesium 高度图 Demo 的 SDK 化实现,包括矩形绘制、最高可用地形层级解析、规则网格采样、灰度高度图生成、起伏 Primitive 构建和地形深度测试。
Kevin
这篇笔记解决什么问题
这个 Demo 的目标不是把一次性逻辑堆到页面里,而是把“从地形生成高度图”拆成可以复用的 SDK 模块:
rectangleDraw.js只负责地形拾取和矩形绘制terrainSampler.js只负责最高层级解析和规则网格采样heightmapCanvas.js只负责把高度数组转成灰度 canvasheightmapSurface.js只负责创建带真实高度顶点的 Cesium PrimitiveCesiumTerrainHeightmapDemo.js负责把这些能力组装成 Demo runtime
这样后续如果要把高度图接入水流模拟、侵蚀模拟或碰撞计算,可以复用采样和 canvas 输出模块,而不需要复制整页交互。
整体调用链
页面启动后先通过统一 runtime 加载 Cesium,再创建 Viewer 和 World Terrain。GUI 暴露三个入口:绘制提取范围、切换地形深度测试、加载或取消地形。
gui.add(controls, 'drawRange').name('绘制提取范围')
gui.add(controls, 'terrainDepthTest').name('地形深度测试')
gui.add(controls, 'toggleTerrain').name('加载 / 取消地形')
用户完成矩形绘制后,runtime 会顺序执行:
sampleTerrainHeightmap(viewer, { rectangle })renderTerrainHeightmapCanvas(sampleResult)createTerrainHeightmapSurface(viewer, sampleResult, heightmap.canvas)heightmapPanel.update(sampleResult, heightmap)
这条链路保证左下角预览图和场景中的起伏面使用同一份高度结果。
矩形绘制怎么做
绘制模块使用 Cesium 的屏幕事件系统,第一次左键记录起点,鼠标移动时更新预览矩形,第二次左键完成范围。拾取点优先走 globe.pick,这样鼠标落在真实地形上时能得到地形表面位置。
const ray = viewer.camera.getPickRay(windowPosition)
return viewer.scene.globe.pick(ray, viewer.scene) ||
viewer.camera.pickEllipsoid(windowPosition, viewer.scene.globe.ellipsoid)
完成时会把两个角点归一化为 Cesium.Rectangle:
const west = Math.min(startCartographic.longitude, endCartographic.longitude)
const east = Math.max(startCartographic.longitude, endCartographic.longitude)
const south = Math.min(startCartographic.latitude, endCartographic.latitude)
const north = Math.max(startCartographic.latitude, endCartographic.latitude)
return new Cesium.Rectangle(west, south, east, north)
当前版本不处理跨国际日期变更线的特殊矩形,适合常规局部地形提取。
最高层级采样怎么做
参考文章里的核心思想是“先找矩形范围内最高可用地形瓦片,再把高度写进高度图”。这里为了保持 Demo 稳定,使用 Cesium 公开 API 获取范围内整体可用最高层级:
const availability = terrainProvider?.availability
const level = availability?.computeBestAvailableLevelOverRectangle?.(rectangle)
if (Number.isFinite(level)) {
return Math.max(0, Math.floor(level))
}
拿到层级后,模块按矩形宽高比生成规则采样网格,最大边默认限制为 96 个采样点,防止一次请求过大范围时拖垮页面。
const { columns, rows } = resolveGridSize(Cesium, rectangle, maxGridSide)
const positions = createGridPositions(Cesium, rectangle, columns, rows)
await Cesium.sampleTerrain(terrainProvider, level, positions, false)
sampleTerrain() 会按瓦片组织请求,并调用对应 TerrainData.interpolateHeight() 得到每个经纬度点的地形高度。局部缺失的高度会用邻域高度补齐,保证后续 canvas 和网格不会出现空洞。
高度图 canvas 怎么生成
heightmapCanvas.js 会根据矩形投影宽高比创建 canvas,最大边默认 512px。每个像素通过双线性插值从采样网格中读取高度,再按当前范围内的最小值和最大值归一化成灰度。
const height = sampleHeightBilinear(sampleResult, u, v)
const intensity = normalizeHeight(height, sampleResult.minHeight, sampleResult.heightRange)
const value = clampInteger(intensity * 255, 0, 255)
这里输出的是当前范围内的相对灰度高度图:黑色代表当前范围内较低区域,白色代表较高区域。左下角面板直接挂载这张 canvas,场景中的起伏面也把同一张 canvas 当作 Image 材质使用。
起伏面怎么创建
场景面不是贴地影像,也不是 GroundPrimitive。它是一个真实三角网格:每个采样点都会转换成带高度的 Cartesian3,并按规则网格生成三角索引。
const height = sampleResult.heights[index] + heightOffset
const cartesian = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, height)
positions[positionOffset] = cartesian.x
positions[positionOffset + 1] = cartesian.y
positions[positionOffset + 2] = cartesian.z
纹理坐标按西到东、北到南映射,保证 canvas 高度图和矩形范围方向一致。
textureCoordinates[stOffset] = columnRatio
textureCoordinates[stOffset + 1] = 1 - rowRatio
最后用 GeometryPipeline.computeNormal() 计算法线,再交给 MaterialAppearance + Image material 渲染。
return Cesium.GeometryPipeline.computeNormal(geometry)
这个面已经拥有自己的地形高度顶点,所以即使后续隐藏或替换 Cesium 地形,它仍然保留原本采样到的凹凸起伏。
为了避免生成面像半透明覆盖层一样透视底下地形,canvas 输出使用满 alpha,材质也走不透明渲染并开启深度写入。
return Cesium.Material.fromType('Image', {
image: canvas,
repeat: new Cesium.Cartesian2(1, 1),
color: Cesium.Color.WHITE,
transparent: false
})
appearance: new Cesium.MaterialAppearance({
material,
materialSupport: Cesium.MaterialAppearance.MaterialSupport.TEXTURED,
translucent: false,
faceForward: true,
renderState: {
depthTest: { enabled: true },
depthMask: true,
cull: { enabled: false }
}
})
地形深度测试的作用
地形深度测试 按钮只切换一个 Cesium 场景状态:
viewer.scene.globe.depthTestAgainstTerrain = nextValue
这个 Demo 默认开启地形深度测试,让生成面一开始就和真实地形保持正确遮挡关系。
viewer.scene.globe.depthTestAgainstTerrain = true
关闭时,高度图面更容易完整观察;开启时,真实地形会参与深度遮挡,可以用来判断生成面是否确实贴近当前地形。为了减少完全共面导致的闪烁,生成面只增加 0.35m 的显示偏移,高度变化仍来自真实采样结果。
地形加载开关
加载 / 取消地形 按钮用于在 Cesium World Terrain 和椭球地形之间切换。取消加载时只替换 viewer.terrainProvider,不会销毁已经生成的高度图 Primitive,所以可以验证这个面是否真的拥有自己的高度顶点。
viewer.terrainProvider = new Cesium.EllipsoidTerrainProvider()
viewer.scene.globe.enableLighting = false
重新加载时会复用已经创建过的 World Terrain provider;如果还没有缓存,则再次调用 createWorldTerrainAsync()。
生命周期清理
Demo 销毁时会统一清理绘制事件、结果 Primitive、范围矩形、左下角面板和 Cesium Viewer。
drawTool.destroy()
clearPreviousResult()
heightmapPanel.destroy()
if (!viewer.isDestroyed()) {
viewer.destroy()
}
这一点对内容站很重要:Demo 详情页通过路由切换反复挂载和卸载,如果不清理 ScreenSpaceEventHandler 或 Primitive,很容易出现旧事件残留和显存占用增长。