📌 专栏连载提醒:本文是《CesiumJS 从入门到精通》第 17 篇,建议按专栏顺序阅读,完整搭建三维 GIS 知识体系
写在前面
上一章讲解影像栅格图层,实现地球表面纹理贴图,但无高程起伏的地球只是二维贴图。地形 DEM 高程数据是三维地球的骨架,赋予山脉、盆地、平原真实高差立体感。 本章完整讲解 Cesium 地形系统:地形瓦片原理、TerrainProvider 各类数据源加载、地形光照、垂直夸张放大、高程采样测量、高度参考贴地逻辑,配套完整可运行综合 Demo,覆盖地形可视化、地形分析、模型贴地等数字孪生高频场景。
本章学习目标
- 理解 DEM 数字高程模型、quantized-mesh 地形瓦片四叉树 LOD 加载机制
- 熟练使用各类
TerrainProvider,加载 Cesium Ion 全球地形、自定义本地地形服务 - 掌握
enableLighting地形光照,强化山体明暗立体效果 - 通过
verticalExaggeration垂直夸张系数放大微地形起伏 - 使用
sampleTerrain异步采样任意经纬度精确海拔,实现地形剖面分析 - 吃透
HeightReference高度参考枚举,解决模型埋地 / 悬空问题,实现要素贴地渲染
一、地形数据底层原理:DEM 高程网格
1.1 DEM 数字高程模型概念
DEM(数字高程模型):用二维网格存储每个坐标点海拔高度,区别于影像 RGB 颜色值,像素存储米级高程,负数代表海平面以下。 类比:影像 = 地球皮肤纹理;地形 DEM = 地球骨骼高差。
1.2 Cesium quantized-mesh 专用地形瓦片
Cesium 自研量化网格地形格式,对比普通 GeoTIFF 优势:
- 四叉树分层 LOD:远距离粗瓦片、近距离高精度瓦片,按需加载
- 内置顶点法线,支持实时太阳光阴影渲染
- 附带水面掩码,实现湖泊、海洋水面特效
- 二进制轻量化压缩,传输体积远小于原始 DEM
1.3 地形瓦片加载流程
全球地形四叉树分层递归切割,从根节点全局低精度瓦片,逐级细分至局部厘米 / 米级高精度瓦片,相机移动自动调度、缓存、卸载瓦片。
二、TerrainProvider 地形数据源加载器
Cesium 采用多实现类统一抽象地形加载,切换数据源无需改动渲染逻辑:
| 类名 | 适用场景 | 数据源 |
|---|---|---|
| EllipsoidTerrainProvider | 无地形平地模式 | 内置椭球面,所有高程 = 0 |
| CesiumTerrainProvider | 最常用,quantized-mesh 标准 | Cesium Ion 全球地形 / 自研地形服务 |
| ArcGISTiledElevationTerrainProvider | 对接 ArcGIS 高程服务 | ArcGIS Server 在线高程 |
| VRTheWorldTerrainProvider | 全球公开简易地形 | VR-TheWorld 公开接口 |
2.1 默认平地椭球地形
无任何高程起伏,适合平面地图展示:
viewer.terrainProvider = new Cesium.EllipsoidTerrainProvider();2.2 Cesium Ion 全球标准地形(项目首选)
assetId=1 为 Cesium 全球免费地形,需配置 Ion Token:
Cesium.Ion.defaultAccessToken = "你的Ion密钥"; // 简化写法(Cesium1.106+) const worldTerrain = Cesium.Terrain.fromWorldTerrain({ requestVertexNormals: true, // 开启法线,光照生效 requestWaterMask: true // 水面掩码 }); viewer.terrainProvider = worldTerrain;2.3 自建本地 quantized-mesh 地形服务
const customTerrain = new Cesium.CesiumTerrainProvider({ url: "https://xxx.com/terrain/tileset.json", requestVertexNormals: true, requestWaterMask: true }); viewer.terrainProvider = customTerrain;核心配置参数:
- requestVertexNormals:请求顶点法线,地形光照必须开启
- requestWaterMask:开启水面识别,实现湖面、海面渲染
- ellipsoid:默认 WGS84 椭球,一般无需修改
三、enableLighting 地形太阳光立体光照
3.1 光照作用
默认关闭光照时山体无明暗区分,灰蒙蒙一片;开启后根据太阳方位计算坡面明暗,山脊、沟壑立体感大幅提升。 性能:轻微增加 GPU 着色计算,大屏、PC 端建议常开,低配移动端按需关闭。
3.2 基础开关
// 全局开启地形光照 viewer.scene.globe.enableLighting = true; // 初始化Viewer时直接配置 const viewer = new Cesium.Viewer("cesiumContainer", { terrain: Cesium.Terrain.fromWorldTerrain(), globe: { enableLighting: true } });3.3 动态切换 + 自定义太阳光强度 / 方位
// 按钮切换光照 document.getElementById("lightBtn").onclick = () => { viewer.scene.globe.enableLighting = !viewer.scene.globe.enableLighting; }; // 调整全局光照强度 viewer.scene.light.intensity = 1.5; // 自定义太阳方向,模拟早中晚光照 function setSunHour(hour) { const rad = Cesium.Math.toRadians(hour / 24 * 360 - 90); viewer.scene.light.direction = new Cesium.Cartesian3(Math.cos(rad),0.3,Math.sin(rad)); } setSunHour(14); // 下午两点阳光四、verticalExaggeration 地形垂直夸张系数
4.1 功能说明
对地形高程统一缩放倍数,解决平原、微地形高差过小看不清的问题:
- 0.1~0.9:压缩地形,弱化起伏
- 1.0:真实比例(默认)
- 1.5~5:放大微地形,地质、河道分析常用
5:极端夸张,仅演示特效,易出现穿模
4.2 基础用法
viewer.scene.verticalExaggeration = 3; // 高程放大3倍4.3 滑块实时调节完整 HTML+JS
<div style="position:absolute;bottom:20px;left:10px;z-index:999;background:rgba(0,0,0,0.7);color:#fff;padding:10px;border-radius:6px;"> 地形夸张:<span id="exVal">1.0</span>x <input type="range" min="0.1" max="5" step="0.1" value="1" id="exSlider"> </div>const slider = document.getElementById("exSlider"); slider.oninput = e => { const val = parseFloat(e.target.value); document.getElementById("exVal").innerText = val.toFixed(1); viewer.scene.verticalExaggeration = val; };五、sampleTerrain 高程异步采样测量
5.1 应用场景
单点海拔测量、两点高差计算、地形剖面线、淹没分析、飞行器安全高度判断。 特性:异步请求地形瓦片,批量支持多点同时采样,返回精确米级高程。
5.2 单点采样示例(珠峰高程)
// 经纬度转弧度坐标 const point = Cesium.Cartographic.fromDegrees(86.9250,27.9881); Cesium.sampleTerrain(viewer.terrainProvider, 12, [point]) .then(res => { const height = res[0].height; console.log("采样海拔:", height.toFixed(2), "米"); }) .catch(err => console.error("采样失败", err));参数说明:
- terrainProvider:当前场景地形源
- level:采样瓦片层级(9~14,层级越高精度越高、请求越多)
- positions:Cartographic 弧度坐标数组,支持批量多点
5.3 批量采样生成地形剖面
两点之间均匀生成采样点,批量获取高程数据,可结合 ECharts 绘制剖面图:
async function getTerrainProfile(lon1,lat1,lon2,lat2,count=100) { const points = []; for(let i=0;i<=count;i++){ const t = i/count; const lon = Cesium.Math.lerp(lon1,lon2,t); const lat = Cesium.Math.lerp(lat1,lat2,t); points.push(Cesium.Cartographic.fromDegrees(lon,lat)); } const data = await Cesium.sampleTerrain(viewer.terrainProvider,12,points); return data.map(item=>({ lon:Cesium.Math.toDegrees(item.longitude), lat:Cesium.Math.toDegrees(item.latitude), height:item.height })); } // 调用:拉萨-日喀则剖面 getTerrainProfile(91.1320,29.6604,88.8910,29.2669,100).then(profile=>console.log(profile));5.4 采样注意事项
- 平地 EllipsoidTerrainProvider 采样高度恒为 0,无实际意义
- 高频循环采样需做缓存,避免大量重复网络请求
- 必须异步 await/then 获取高程,同步读取会返回空值
六、HeightReference 高度参考(模型贴地核心)
6.1 两类核心枚举
- HeightReference.NONE:高度基于 WGS84 椭球面,0 = 海平面,山区模型易埋入山体 / 高空悬空
- HeightReference.CLAMP_TO_GROUND:高度基于地形地表,0 = 紧贴地面,自动跟随地形起伏
6.2 实体模型贴地示例(飞机模型)
viewer.entities.add({ position:Cesium.Cartesian3.fromDegrees(103.9470,30.5728,0), model:{ uri:"airplane.glb", heightReference:Cesium.HeightReference.CLAMP_TO_GROUND, // 关键贴地配置 scale:1 } });逻辑:position 内高度值为相对地面高度,而非绝对海拔,自动贴合起伏地形。
6.3 对比演示:同一坐标两种高度模式
珠峰点位同时创建红色悬空点(NONE)、绿色贴地点(CLAMP_TO_GROUND),直观高差对比:
const lon=86.9250,lat=27.9881; // 椭球面500米,埋在山体内部 viewer.entities.add({ position:Cesium.Cartesian3.fromDegrees(lon,lat,500), point:{pixelSize:15,color:Cesium.Color.RED}, label:{text:"NONE 椭球面高度"} }); // 地形上方500米,悬浮山顶上空 viewer.entities.add({ position:Cesium.Cartesian3.fromDegrees(lon,lat,500), point:{pixelSize:15,color:Cesium.Color.LIME,heightReference:Cesium.HeightReference.CLAMP_TO_GROUND}, label:{text:"CLAMP_TO_GROUND 相对地面高度"} });6.4 贴地折线旧写法
viewer.entities.add({ polyline:{ positions:Cesium.Cartesian3.fromDegreesArrayHeights([103,30,100,104,30.5,200]), clampToGround:true, // 折线强制贴地 width:5,material:Cesium.Color.RED } });七、综合完整 Demo:地形控制面板 + 高程采样
整合光照开关、地形夸张、区域飞行、中心高程采样,开箱即用 HTML 页面:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script src="https://cesium.com/downloads/cesiumjs/releases/1.120/Build/Cesium/Cesium.js"></script> <link href="https://cesium.com/downloads/cesiumjs/releases/1.120/Build/Cesium/Widgets/widgets.css" rel="stylesheet"> <style> html,body,#cesiumContainer{width:100%;height:100%;margin:0;padding:0;overflow:hidden} .tool{position:absolute;top:10px;left:10px;z-index:999;background:rgba(0,0,0,0.75);color:#fff;padding:15px;border-radius:8px;width:300px} .info{position:absolute;bottom:10px;right:10px;color:#0f0;background:rgba(0,0,0,0.7);padding:10px;border-radius:4px;font-family:monospace} </style> </head> <body> <div id="cesiumContainer"></div> <div class="tool"> <h3>地形控制面板</h3> <label><input type="checkbox" id="lightCheck" checked> 地形光照</label> <div style="margin:8px 0"> <label>夸张系数 <span id="exNum">1.0</span>x</label> <input type="range" min="0.1" max="5" step="0.1" value="1" id="exSlide" style="width:100%"> </div> <select id="areaSel" style="width:100%;padding:4px;margin:8px 0"> <option value="everest">珠穆朗玛峰</option> <option value="canyon">科罗拉多大峡谷</option> <option value="plain">华北平原</option> </select> <button id="sampleBtn" style="width:100%;padding:6px">采样屏幕中心海拔</button> </div> <div class="info" id="infoBox">等待地形加载</div> <script> Cesium.Ion.defaultAccessToken = "替换为你的Ion Token"; const viewer = new Cesium.Viewer("cesiumContainer",{ terrain:Cesium.Terrain.fromWorldTerrain({requestVertexNormals:true,requestWaterMask:true}), globe:{enableLighting:true}, animation:false,timeline:false }); const infoBox = document.getElementById("infoBox"); // 光照切换 document.getElementById("lightCheck").onchange = e=>{ viewer.scene.globe.enableLighting = e.target.checked; }; // 地形夸张调节 const exSlide = document.getElementById("exSlide"); exSlide.oninput = e=>{ const v = parseFloat(e.target.value); document.getElementById("exNum").innerText = v.toFixed(1); viewer.scene.verticalExaggeration = v; }; // 区域飞行 const areaMap = { everest:{lon:86.925,lat:27.9881,alt:20000}, canyon:{lon:-112.112,lat:36.097,alt:15000}, plain:{lon:117.2,lat:39.13,alt:50000} }; document.getElementById("areaSel").onchange = e=>{ const a = areaMap[e.target.value]; viewer.camera.flyTo({ destination:Cesium.Cartesian3.fromDegrees(a.lon,a.lat,a.alt), orientation:{heading:0,pitch:Cesium.Math.toRadians(-45),roll:0}, duration:3 }) }; // 屏幕中心采样高程 document.getElementById("sampleBtn").onclick = async ()=>{ const ray = viewer.camera.getPickRay(new Cesium.Cartesian2(viewer.canvas.clientWidth/2,viewer.canvas.clientHeight/2)); const hit = viewer.scene.globe.pick(ray,viewer.scene); if(!hit) return alert("对准地面再采样"); const carto = Cesium.Cartographic.fromCartesian(hit); const [res] = await Cesium.sampleTerrain(viewer.terrainProvider,12,[carto]); infoBox.innerHTML = `经度:${Cesium.Math.toDegrees(res.longitude).toFixed(4)}<br>纬度:${Cesium.Math.toDegrees(res.latitude).toFixed(4)}<br>高程:${res.height.toFixed(2)} m`; } </script> </body> </html>八、性能与调试工具
8.1 地形优化要点
- 低配移动端降低
verticalExaggeration,关闭requestVertexNormals、地形光照 - 大范围宏观浏览调高 sampleTerrain 采样层级 level=9~10,减少请求
- 高精度地形分析场景 level=12~14,限制单次采样点数
- 水面特效无用时关闭
requestWaterMask减少瓦片体积
8.2 调试参数
viewer.scene.debugShowFramesPerSecond = true; // 显示实时帧率 viewer.scene.globe.tileLoadProgressEvent; // 监听瓦片加载进度本篇总结
- 地形核心载体为 DEM 高程模型,Cesium 采用 quantized-mesh 四叉树瓦片分层 LOD 加载,兼顾精度与加载性能。
CesiumTerrainProvider是主流地形加载器,支持 Ion 全球地形与自研本地地形服务,法线、水面掩码按需开启。enableLighting太阳光阴影强化山体立体感;verticalExaggeration垂直放大微地形,适配地质、河道分析场景。sampleTerrain异步批量采样任意坐标海拔,可实现单点测量、地形剖面线等高阶 GIS 分析功能。HeightReference两种高度参考模式,CLAMP_TO_GROUND实现模型、折线、点位自动贴地,解决山区埋地 / 悬空问题。- 配套完整可交互地形控制面板 Demo,覆盖地形可视化、高程测量、动态参数调节全业务需求。
课后实操练习
- 数据源切换:分别切换 Ellipsoid 平地、Ion 全球地形,对比视觉差异与加载速度。
- 日照动画:定时器循环修改太阳方位,模拟 24 小时日照变化,观察山体光影变化。
- 地形剖面工具:两点绘制采样剖面,使用 Canvas 绘制高程折线图。
- 模型贴地对比:同一山区坐标分别使用 NONE/CLAMP_TO_GROUND 放置 GLB 模型,直观区分两种高度模式。
- 图层组合:结合第 16 章影像图层,实现多影像 + 多地形源切换管理器。
下一篇预告
CesiumJS 从入门到精通 18:万物生长的法则 ——3D Tiles 与 Batched 3D Model
详解 b3dm 瓦片格式原理、批量模型存储结构、倾斜摄影 / BIM 数据解析,结合要素属性表、样式着色实现城市三维建筑可视化。,