基于 Cesium + 天地图的三维地理可视化:以“岳麓山名人墓“为例,详解地形夸张、自动环绕与标签碰撞避让 一、背景与效果预览做 geographic 类可视化时最常遇到的三个工程难点是地形看不见岳麓山平均海拔只有 300m 左右在椭球地球上几乎是一个平面突起不夸张根本看不出山脉起伏标注悬浮开了地形夸张之后手算的高程和渲染网格对不齐POI 标签飘在半空标签打架名人墓密集分布旋转视角时成片标签重叠可读性归零。本文用一个完整可运行的 Demo岳麓山名人墓三维展示把这三个问题一次性解决并给出逐行拆解的工程实现。最终效果地形夸张 ×3岳麓山轮廓立体清晰标注用CLAMP_TO_GROUND钳制到渲染后地表绝不悬浮点开始旋转相机绕山脉平缓环绕约 90 秒一圈不眩晕开启标签避让后重叠面板自动向上抬升错开全部可读。以下截图均来自本实例本地运行index.html检索范围岳麓山周边未做任何后期修饰。二、技术选型与环境能力选型说明三维引擎天地图定制版 CesiumCesium.Map国内直接使用无需翻墙影像底图天地图img_wWeb 墨卡托影像地形高程天地图GeoTerrainProvider真实 DEM地名注记天地图GeoWTFS三维地名随地形起伏Token天地图开发者密钥免费申请注意天地图的Cesium.Map是对Cesium.Viewer的封装API 与官方 Cesium 基本一致GeoTerrainProvider、GeoWTFS是天地图扩展类需引用天地图 CDN 的Cesium.js。三、工程结构index.html # 容器 UI 引入天地图 Cesium js/main.js # 主逻辑本文重点 js/DragLable.js # 标牌拖拽组件位置偏移管理index.html的最小骨架!DOCTYPEhtmlhtmllangzh-CNheadmetacharsetutf-8/title岳麓山名人墓三维展示/titlelinkrelstylesheethrefhttps://api.tianditu.gov.cn/cdn/demo/sanwei/static/cesium/Widgets/widgets.css/style#cesiumContainer{position:absolute;inset:0;}/style/headbodydividcesiumContainer/divdividuibuttonidbtnSpin开始旋转/buttonlabelinputtypecheckboxidchkCollision/标签避让/label/div!-- 天地图 Cesium含 GeoTerrainProvider / GeoWTFS --scriptsrchttps://api.tianditu.gov.cn/cdn/demo/sanwei/static/cesium/Cesium.js/scriptscriptsrcjs/DragLable.js/scriptscriptsrcjs/main.js/script/body/html四、核心一地形夸张 贴地标注4.1 地形夸张与遮挡这是看得清山脉的前提。两行配置解决// 开启地形夸张数值越大越夸张1 为原始地形viewer.scene.globe.terrainExaggeration3;// 启用贴地深度检测POI 与地形正确遮挡旋转视角时位置不漂移viewer.scene.globe.depthTestAgainstTerraintrue;depthTestAgainstTerrain true是关键中的关键它让 billboard 参与 GPU 深度测试位于山体之后的标注会被地形自然遮住而不是穿透显示在最上层。4.2 地形 Providervartoken你的天地图Key;vartdtUrlhttps://t{s}.tianditu.gov.cn/;varsubdomains[0,1,2,3,4,5,6,7];// 构造多子域地形地址负载均衡varterrainUrlssubdomains.map(function(s){returntdtUrl.replace({s},s)mapservice/swdx?Telv_ctktoken;});viewer.terrainProvidernewCesium.GeoTerrainProvider({urls:terrainUrls});4.3 为什么手算高程会悬浮直觉做法是globe.getHeight()取真实高程再乘以terrainExaggeration作为 billboard 的绝对高度。但在天地图这套 Cesium 里渲染网格着色器内缩放与getHeight返回值的夸张处理并不一致——要么getHeight已经含夸张要么网格本身未缩放结果就是标注被放到约 3 倍高度处肉眼可见地飘在半空。4.4 正确做法CLAMP_TO_GROUND钳制到地表把算高度这件事交给 Cesium 自己。Billboard 加一个heightReference引擎会自动把实体钳制到渲染后的地形表面已包含夸张与高度计算彻底解耦varpinviewer.entities.add({position:groundPos,// 高度字段被钳制忽略仅用经纬度billboard:{image:poiDotImage,width:DOT_SIZE,height:DOT_SIZE,pixelOffset:newCesium.Cartesian2(0,0),// 关键钳制到渲染后地表无论地形怎么夸张都不悬浮heightReference:Cesium.HeightReference.CLAMP_TO_GROUND}});工程经验凡是必须贴着地形的标注优先用CLAMP_TO_GROUND不要自己算高度。手算高度只在需要做屏幕投影锚点碰撞布局、背面剔除时保留且它只影响投影不再决定垂直位置因此即便有误差也不会导致悬浮。五、核心二自动环绕旋转绕点飞行自动旋转本质是让相机围绕一个目标点按固定仰角、匀速转动。Cesium 提供了camera.lookAt(target, HeadingPitchRange)这一组合拳。5.1 状态与参数varisSpinningfalse;varspinTargetnull;varspinHeading0;varspinPitchCesium.Math.toRadians(-35);// 固定俯仰角俯视山脉varspinRange8000;// 环绕半径米varspinSpeedCesium.Math.toRadians(4);// 角速度4°/秒 ≈ 90 秒一圈var_lastSpinTimenull;5.2 每帧按真实时间差推进角度旋转必须基于**真实时间差dt**推进而不是每帧 固定值否则高刷新率屏幕转得快、低刷新率转得慢。在preRender里做viewer.scene.preRender.addEventListener(function(){if(!isSpinning||!spinTarget)return;varnowperformance.now();if(_lastSpinTimenull){_lastSpinTimenow;return;}// 起转首帧不跳变vardt(now-_lastSpinTime)/1000;// 秒_lastSpinTimenow;spinHeadingspinSpeed*dt;// 角速度 × 时间viewer.camera.lookAt(spinTarget,newCesium.HeadingPitchRange(spinHeading,spinPitch,spinRange));});5.3 开始 / 停止functionstartSpin(target){spinTargettarget||getViewerCenter()||// 以视野中心为环绕目标Cesium.Cartesian3.fromDegrees(112.935,28.169);spinRangeCesium.Cartesian3.distance(viewer.camera.position,spinTarget);_lastSpinTimenull;// 重置计时避免起转跳变isSpinningtrue;document.getElementById(btnSpin).textContent停止旋转;}functionstopSpin(){isSpinningfalse;viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);// 解除锁定恢复自由控制document.getElementById(btnSpin).textContent开始旋转;}getViewerCenter()用屏幕中心射线拾取地形保证绕的是你正在看的那座山而不是椭球面上的一个假点functiongetViewerCenter(){varcanvasviewer.scene.canvas;varcenternewCesium.Cartesian2(canvas.clientWidth/2,canvas.clientHeight/2);varrayviewer.camera.getPickRay(center);varposviewer.scene.globe.pick(ray,viewer.scene);returnCesium.defined(pos)?pos:viewer.camera.pickEllipsoid(center,viewer.scene.globe.ellipsoid);}六、核心三标签碰撞避让不隐藏只错位名人墓彼此离得很近旋转时标签必然重叠。我们的策略是不隐藏、只抬升重叠的面板在静止位置基础上逐级向上平移直到在屏幕上错开。6.1 屏幕投影 重叠检测核心思路把每个标注的地面锚点投影到屏幕坐标系得到面板矩形的左上角再用经典的轴对齐包围盒相交判定决定是否与已布局的面板重叠。varPANEL_RAISE_STEP22;// 每次额外上移像素varPANEL_MAX_RAISE12;// 最大上移次数封顶防止无限抬升functionupdatePoiVisibility(){varkept[];// 已布局面板的屏幕矩形poiEntities.forEach(function(o){varspviewer.scene.cartesianToCanvasCoordinates(o.groundPos,newCesium.Cartesian2());if(!Cesium.defined(sp)){// 位于相机背面 → 整体隐藏o.origin.billboard.showfalse;o.sign.billboard.showfalse;return;}o.origin.billboard.showtrue;o.sign.billboard.showtrue;if(!collisionEnabled)return;// 未开启避让保持原偏移varwo.panelW,ho.panelH,baseo.baseOffset;varraise0,steps0,overlaptrue,rectnull;while(stepsPANEL_MAX_RAISE){// 面板锚点左中x 锚点 偏移y 中心再叠加垂直避让量varleftsp.xbase.x;varcenterYsp.y-base.y-raise;rect{x:left,y:centerY-h/2,w:w,h:h};overlapkept.some(function(k){// AABB 不相交判定取反即为相交return!(rect.xrect.wk.x||rect.xk.xk.w||rect.yrect.hk.y||rect.yk.yk.h);});if(!overlap)break;// 不再重叠定稿raisePANEL_RAISE_STEP;steps;// 仍重叠继续上移}dragLable.applyOffset(o,newCesium.Cartesian2(base.x,base.yraise));kept.push(rect);});}6.2 何时刷新相机每次移动含自动旋转都要重算——在postRender里加一个相机位移节流即可避免每帧空转var_lastCamPosnull;viewer.scene.postRender.addEventListener(function(){varcpviewer.camera.position;if(_lastCamPosCesium.Cartesian3.equalsEpsilon(cp,_lastCamPos,1e-3))return;_lastCamPosCesium.Cartesian3.clone(cp,_lastCamPos);updatePoiVisibility();});被山体遮挡的隐藏由 GPU 深度测试负责见 4.1CPU 这里只管屏幕上是否可见 是否重叠职责清晰、性能可控。七、名人墓数据集与标注渲染把通用检索换成一份内置的岳麓山名人墓数据坐标均为约值建议在工程中按测绘资料校正// 岳麓山名人墓坐标为近似位置实际部署请校正varYUELU_TOMBS[{name:黄兴墓,lon:112.9365,lat:28.1695,who:辛亥革命元勋中华民国开国元勋},{name:蔡锷墓,lon:112.9338,lat:28.1710,who:护国将军再造共和之功臣},{name:焦达峰墓,lon:112.9345,lat:28.1680,who:辛亥长沙起义领袖},{name:陈天华姚宏业墓,lon:112.9352,lat:28.1672,who:革命宣传家《猛回头》作者},{name:禹之谟墓,lon:112.9370,lat:28.1700,who:近代资产阶级革命家},{name:刘道一墓,lon:112.9330,lat:28.1665,who:同盟会会员萍浏醴起义烈士},{name:蒋翊武墓,lon:112.9360,lat:28.1688,who:武昌起义重要组织者}];用 Canvas 动态生成圆点 名称胶囊两张图2x 高清显示时scale0.5再分别挂为 billboard。原点圆点functioncreatePoiDotImage(){varS2,D14*S;varcanvasdocument.createElement(canvas);canvas.widthcanvas.heightD;varctxcanvas.getContext(2d),cD/2;varglowctx.createRadialGradient(c,c,D*0.28,c,c,c);// 柔光外圈glow.addColorStop(0,rgba(0,160,255,0.35));glow.addColorStop(1,rgba(0,160,255,0));ctx.fillStyleglow;ctx.beginPath();ctx.arc(c,c,c,0,Math.PI*2);ctx.fill();ctx.fillStyle#fff;ctx.beginPath();ctx.arc(c,c,c-1.5,0,Math.PI*2);ctx.fill();varcorectx.createRadialGradient(c-c*0.3,c-c*0.3,c*0.15,c,c,c-3);core.addColorStop(0,#9beeff);core.addColorStop(0.5,#22b7f5);core.addColorStop(1,#0072c6);ctx.fillStylecore;ctx.beginPath();ctx.arc(c,c,c-3,0,Math.PI*2);ctx.fill();returncanvas.toDataURL(image/png);}名称面板圆角胶囊 深蓝渐变思路相同使用createPoiNameImage(name)返回{ url, w, h }w/h为显示尺寸供碰撞布局估算屏幕矩形。添加标注functionaddPoiToCesium(t){vargroundPosCesium.Cartesian3.fromDegrees(t.lon,t.lat);// 高度被 CLAMP 忽略varpinviewer.entities.add({position:groundPos,billboard:{image:poiDotImage,width:14,height:14,heightReference:Cesium.HeightReference.CLAMP_TO_GROUND},description:div stylemin-width:200pxbt.name/bbr/(t.who||)/div});varpanelImgcreatePoiNameImage(t.name);varcardviewer.entities.add({position:groundPos,billboard:{image:panelImg.url,scale:0.5,horizontalOrigin:Cesium.HorizontalOrigin.LEFT,verticalOrigin:Cesium.VerticalOrigin.CENTER,heightReference:Cesium.HeightReference.CLAMP_TO_GROUND}});varodragLable.addEntity(pin,card,null,newCesium.Cartesian2(0,0));o.panelWpanelImg.w;o.panelHpanelImg.h;o.lont.lon;o.latt.lat;o.groundPosgroundPos;o.namet.name;poiEntities.push(o);}// 初始化飞到岳麓山上空并标注functioninitTombScene(){viewer.camera.flyTo({destination:Cesium.Cartesian3.fromDegrees(112.935,28.169,9000),orientation:{heading:0,pitch:Cesium.Math.toRadians(-55),roll:0}});YUELU_TOMBS.forEach(addPoiToCesium);updatePoiVisibility();}initTombScene();最后给出完整的视频链接感兴趣的朋友可以自由查看也欢迎大家来长沙打卡来岳麓山打卡。Cesium盘点岳麓山和上面的那些人八、踩坑总结重点地形夸张下标注悬浮不要手算getHeight × exag直接heightReference: CLAMP_TO_GROUND。渲染网格与getHeight的夸张语义在该 Cesium 版本里不一致手算必飘。标注穿透山体必须depthTestAgainstTerrain true否则 billboard 永远画在最上层看起来像悬浮穿透。自动旋转眩晕/跳变用lookAt HeadingPitchRange角度基于performance.now()的时间差推进起转首帧重置计时避免瞬间跳一大段。停止旋转后相机失控stopSpin必须camera.lookAtTransform(Matrix4.IDENTITY)解除锁定。标签重叠CPU 端用屏幕投影 AABB 做抬升避让不隐藏被地形遮挡的隐藏交给 GPU 深度测试两者分离性能与可读性兼顾。地形未加载时高度采样为 0tileLoadProgressEvent在瓦片加载完成后清空高度缓存并重算groundPos避免初始贴地错位。九、结语地形夸张 自动旋转 标签碰撞是三维地理展示的三件套。本文以岳麓山名人墓为切口给出了可直接落地的 Cesium 实现贴地靠CLAMP_TO_GROUND环绕靠lookAt/HeadingPitchRange避让靠屏幕投影 AABB。三者组合后既能看清山脉脉络又能在动态旋转中保持标签清晰可读。代码已脱敏整理核心逻辑均来自生产级 Demo。如果你在做红色景点、名人故居、陵园纪念碑等同类项目这套骨架可以直接复用。本文示例代码基于天地图定制版 Cesium部分扩展类GeoTerrainProvider/GeoWTFS为天地图专有请引用其官方 CDN。