资讯动态

Cesium接入天地图实现业务标记:从Entity到Primitive的性能优化实践

发布时间:2026/10/3 13:23:47 来源:尧图企业网站定制
去年接了个三维可视化项目需求看起来很简单在Cesium里把全国几个样板河段的管理点位标出来点击看实时水位、监控视频和巡河记录。结果第一版做完自己都看不下去——底图用的在线影像在部分地区分辨率拉胯标注点一多就卡顿更别提点击弹窗这种交互了。后来把天地图作为底图接进来又把标记这一整套从Entity换成Primitive架构重新梳理一遍才算真正能用在业务上。这篇就把Cesium里用天地图做底图、实现业务标记的完整过程写出来从底图选型、坐标系适配、标记体系搭建到性能优化和单体化联动都过一遍。适合已经在用Cesium做项目、但觉得标记功能“能用但不好用”的人也适合刚接触Cesium、想直接抄一套能落地的标记方案的开发。文章里涉及的代码都是可以在项目中直接改改用的不是Demo级别的玩具。1. 天地图那张底图到底比在线影像强在哪先说选型。现在很多人一上来就用Cesium官方默认的在线影像服务毕竟零配置、打开就能跑。但在实际业务里这么做经常出问题国外影像源在国内访问不稳定加载速度忽快忽慢遇到城市级高精度查看需求时某些区域影像更新慢甚至能看到明显的拼接痕迹。更关键的是国内业务对地图服务的合规性要求很高项目验收的时候你得能说清楚底图是从哪个权威渠道来的。天地图的优势就在这几点上体现得比较明显。首先是权威性。天地图是国家地理信息公共服务平台底图数据来自各级测绘地理信息部门坐标体系用的是CGCS2000中国2000国家大地坐标系。做国内的项目拿天地图当底图数据出处说得清合规问题不用操心。其次是免费额度对中小项目很够用。天地图开放平台提供多种瓦片服务个人和企业只要注册个账号申请Token就是大家常说的tk就能拿到标准WMTS服务地址。一般中小型项目每天几万次的瓦片请求量完全在免费范围内。再者天地图在国内的访问速度和更新频率是很多商业图源比不了的。高程地形服务、影像服务、矢量服务都有专门的瓦片地址按需加载配置起来不绕。不过要注意一点天地图本身提供的是瓦片服务不是现成的Cesium图层。你得在Cesium里把它的WMTS接口拼进自己的底图体系。在动手之前先去天地图官网注册账号在控制台里创建一个应用拿到tk密钥。这一步不用写代码但很多人都卡在“不知道去哪找tk”其实就在官网开放平台的“控制台—应用管理”里创建应用后自动生成。还有一点值得提前说清楚天地图在线服务需要联网访问如果你的项目部署在纯内网环境得先准备好离线瓦片方案。常见做法是拿天地图的瓦片工具把所需区域的底图切片缓存到本地或用GeoServer重新发布。这块涉及面比较大这章先不展开项目确实跑在内网的话建议单独用一套离线切片方案。2. 底图接入实操影像、注记、地形一次配齐天地图的WMTS服务地址是标准REST风格Cesium官方有对应的封装类不需要写底层的瓦片解析逻辑。下面直接给出我项目里验证过的核心配置。2.1 影像和注记图层的标准配置先在开发环境里装好Cesium依赖。我是按官方推荐方式引入整个Cesium包npm install cesium代码里建议直接把Cesium的静态资源通过CESIUM_BASE_URL指到自己的目录或者用vite-plugin-cesium这类插件处理避免资源路径报错。基础配置完成后接入天地图影像图层的核心代码如下const viewer new Cesium.Viewer(cesiumContainer, { animation: false, timeline: false, baseLayerPicker: false, geocoder: false, homeButton: false, sceneModePicker: false, navigationHelpButton: false, infoBox: false }); const TIANDITU_TOKEN 你的天地图tk; // 影像底图 const imgProvider new Cesium.UrlTemplateImageryProvider({ url: https://t{0-7}.tianditu.gov.cn/img_w/wmts?SERVICEWMTS REQUESTGetTileVERSION1.0.0 LAYERimgSTYLEdefaultTILEMATRIXSETw FORMATtilesTILEMATRIX{z}TILEROW{y}TILECOL{x} tk TIANDITU_TOKEN, subdomains: [0, 1, 2, 3, 4, 5, 6, 7], maximumLevel: 18 }); // 影像注记叠加在影像底图上的中文地名、道路名 const ciaProvider new Cesium.UrlTemplateImageryProvider({ url: https://t{0-7}.tianditu.gov.cn/cia_w/wmts?SERVICEWMTS REQUESTGetTileVERSION1.0.0 LAYERciaSTYLEdefaultTILEMATRIXSETw FORMATtilesTILEMATRIX{z}TILEROW{y}TILECOL{x} tk TIANDITU_TOKEN, subdomains: [0, 1, 2, 3, 4, 5, 6, 7], maximumLevel: 18 }); viewer.imageryLayers.addImageryProvider(imgProvider); viewer.imageryLayers.addImageryProvider(ciaProvider);这里有个很多人没注意的细节img_w是影像地址cia_w是影像对应的中文注记层。只加影像不加注记地图上全是卫星图但没有地名标注业务人员根本没法用两个图层要一起加并且注记层叠加顺序在影像层之后Cesium的addImageryProvider默认就满足这个顺序。如果项目需要的是“电子地图风格”而不是卫星影像把img_w替换成vec_wcia_w替换成cva_w其他参数不用动// 矢量底图 const vecProvider new Cesium.UrlTemplateImageryProvider({ url: https://t{0-7}.tianditu.gov.cn/vec_w/wmts?SERVICEWMTS REQUESTGetTileVERSION1.0.0 LAYERvecSTYLEdefaultTILEMATRIXSETw FORMATtilesTILEMATRIX{z}TILEROW{y}TILECOL{x} tk TIANDITU_TOKEN, subdomains: [0, 1, 2, 3, 4, 5, 6, 7], maximumLevel: 18 }); // 矢量注记 const cvaProvider new Cesium.UrlTemplateImageryProvider({ url: https://t{0-7}.tianditu.gov.cn/cva_w/wmts?SERVICEWMTS REQUESTGetTileVERSION1.0.0 LAYERcvaSTYLEdefaultTILEMATRIXSETw FORMATtilesTILEMATRIX{z}TILEROW{y}TILECOL{x} tk TIANDITU_TOKEN, subdomains: [0, 1, 2, 3, 4, 5, 6, 7], maximumLevel: 18 });2.2 地形服务要不要接很多Cesium项目用默认的Ellipsoid地形表面是光滑的球面叠加3dtiles或高程分析时效果会差很多。天地图的高程地形服务可以缓解这个问题官方提供的是一个WMS风格的接口目前主要有两种接法一种是通过Cesium.ArcGisMapServerImageryProvider加载天地图的地形服务部分地区支持另一种是用Cesium.CesiumTerrainProvider配合自定义地形瓦片。实测下来更稳的方案其实是直接用Cesium官方地形或基于天地图高程数据生成的Terrain瓦片。如果你不想在“接地形”上花太多时间又希望倾斜模型和白模贴合地面可以先用官方地形顶着再在建模阶段做底部压平。我自己的做法是底图用天地图地形用Cesium官方Terrain或离线地形瓦片。天地图地形服务在部分区域的效果确实不错但接口稳定性和国际网络环境下还是有波动生产环境优先保稳定地形数据能用即可。2.3 坐标系那点事CGCS2000和WGS84要不要转换天地图用的是CGCS2000坐标系Cesium内部用的是WGS84。很多新手一听“两个坐标系”就慌了以为要做复杂转换。实操中这两个坐标系在绝大多数业务场景下差值在厘米到分米级Cesium做可视化、量算、标绘根本感知不到差异。直接当WGS84用经纬度坐标照常填Cesium.Cartesian3.fromDegrees(lng, lat, height)坐标值不用做任何修正。唯一要注意的是如果项目里接了来自不同部门的数据有的数据用的是地方坐标系或者GCJ02火星坐标系那才需要专门的转换工具类。GCJ02的偏移在几十到几百米量级不能忽略通常用开源工具库把GCJ02转成WGS84再喂给Cesium。天地图的坐标拾取工具返回的是CGCS2000经纬度可以直接用不用二次处理。3. 标记落地Entity方便Primitive保命底图问题解决后就到了标题里最核心的“标记”。Cesium里做标记最简单的是用Entity加BillboardGraphics或PointGraphics。Entity上手快代码直观对少量标记点几十到一两百个性能也够。项目初期我就用这种方式把每个监测点渲染成一个圆形图标加一个名称标签function addSimpleMarker(viewer, lng, lat, name, time) { const entity viewer.entities.add({ id: monitor_${lng}_${lat}, position: Cesium.Cartesian3.fromDegrees(lng, lat, 0), billboard: { image: /markers/monitor.png, width: 32, height: 32, verticalOrigin: Cesium.VerticalOrigin.BOTTOM, disableDepthTestDistance: Number.POSITIVE_INFINITY }, label: { text: name, font: 14px sans-serif, fillColor: Cesium.Color.WHITE, outlineColor: Cesium.Color.BLACK, outlineWidth: 2, style: Cesium.LabelStyle.FILL_AND_OUTLINE, pixelOffset: new Cesium.Cartesian2(0, -36), disableDepthTestDistance: Number.POSITIVE_INFINITY }, properties: { lastUpdate: time } }); return entity; }用Entity做标记有几个好处数据模型自带属性存储properties、点击拾取事件天然支持、增删改查都非常方便适合快速迭代。但一旦标记量级上去问题就来了。我在项目里加载的业务点位大约3000个包含河长制公示牌、雨量站、水位站、排污口等。用Entity全量加载后相机一旋转明显掉帧在某些配置较低的业务电脑上旋转地球甚至直接卡顿好几秒。原因不复杂每个Entity都是一等公民内部会创建独立的渲染状态、DrawCommand几千个DrawCommand同时存在GPU和CPU压力都很大。解决方案就是换用PrimitiveBillboardCollection。它把多个Billboard合批到一次绘制调用里渲染性能比Entity高一个量级。下面是我封装的一组轻量标记工具class MarkerLayer { constructor(viewer) { this.viewer viewer; this.billboards new Cesium.BillboardCollection(); this.viewer.scene.primitives.add(this.billboards); } addMarkers(markerList) { markerList.forEach((item) { const position Cesium.Cartesian3.fromDegrees(item.lng, item.lat, 0); this.billboards.add({ position: position, image: item.icon || /markers/default.png, width: item.width || 32, height: item.height || 32, verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 禁止被地形/模型遮挡确保标记始终可见 disableDepthTestDistance: Number.POSITIVE_INFINITY, // 自定义属性拾取时用 id: item.id, attributes: item.attrs || {} }); }); } removeAll() { this.billboards.removeAll(); } }使用方式const markers new MarkerLayer(viewer); markers.addMarkers([ { id: station_001, lng: 116.40, lat: 39.90, icon: /markers/raindrop.png, attrs: { type: 雨量站 } }, { id: station_002, lng: 116.41, lat: 39.91, icon: /markers/waterlevel.png, attrs: { type: 水位站 } } ]);BillboardCollection里的每个Billboard渲染时走的是一条合并的绘制路径同样3000个点帧率能稳定在50~60fps。这是我在这个项目里最值得的一次重构。如果你连图片都不需要只求最多数量的点标记用PointPrimitiveCollection还能再省一点显存性能更进一步。它只渲染圆形或方形色点样式比较基础适合“只看分布态势”的场景。4. 标记和3dtiles单体化联动点击白模弹窗标记不只是“点一下看到气泡”这么简单。我在这个项目里遇上的真实需求是倾斜摄影和人工白模已经建好业务希望点击模型上的楼层或设备弹出对应监测数据。这就涉及“3dtiles单体化”。Cesium加载3dtiles本身没什么难度const tileset await Cesium.Cesium3DTileset.fromUrl(/data/3dtiles/river_daye/tileset.json); viewer.scene.primitives.add(tileset);麻烦的是“点击某个建筑”你得知道点中的是哪一栋、有没有业务数据。3dtiles里每个Building有batchId需要模型数据在生成时把业务属性写入batchTable。这通常依赖建模团队在数据生产阶段完成。拿到数据后前端可以用Scene.pick来命中const handler new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); handler.setInputAction((movement) { const picked viewer.scene.pick(movement.position); if (Cesium.defined(picked) picked.primitive instanceof Cesium.Cesium3DTileset) { const feature picked.primitive.getFeature(picked.batchId); const propNames feature.getPropertyNames(); const props {}; propNames.forEach((name) { props[name] feature.getProperty(name); }); // 根据属性字段SpringId或BldCode匹配业务标记数据 const bizData findMarkerByCode(props.BldCode); if (bizData) { openMarkerInfoWindow(bizData, movement.position); } } }, Cesium.ScreenSpaceEventType.LEFT_CLICK);单体化的核心不是代码是数据约定。在项目启动阶段就要和建模团队对齐字段命名比如建筑编码统一叫bld_code业务编码统一用monitor_code别一个用BldCode一个用建筑编号前端没法写通用逻辑。我们项目踩过这个坑后面所有单体化模型数据命名都写进验收标准里了。这块和标记系统的配合点在哪选中建筑后弹出的信息窗本质上就是标记点的信息卡片。你可以直接复用标记点的信息窗组件把feature属性塞进去展示水位、水质、责任单位等字段用户看到的交互是一致的。5. 天地图瓦片加载的稳定性滚动崩溃和瓦片错乱的排查网上不少人反馈“Cesium 3D地球滚动出现崩溃”。这个词条我也被搜索到过好几次结合个人经验大致说一下。天地图WMTS服务对并发请求数是有限制的同一时间请求过多会直接拒绝服务表现就是瓦片加载一半突然变灰再滚动还会卡死。这不是Cesium本身的问题是瓦片源限流。解决办法有几个开启Cesium的请求调度Cesium默认有最大Request限制但有时你手动创建多个Primitive会绕过调度器。确保所有影像请求走的是ImageryLayer体系而不要用fetch自己去拼瓦片URL。控制视口内可见图层数量如果底图、注记、业务切片、轨迹切片全部同时可见瓦片请求数量会急剧膨胀。业务图层尽量在低缩放级别隐藏、高缩放级别显示减少不必要的请求。瓦片域名轮询天地图给你提供t0到t7八个子域名用UrlTemplateImageryProvider的subdomains参数全部配置上浏览器会分散请求避免单个域名的并发限制。说到崩溃还有一种情况是标记点太多且每个标记用了独立的Entity、又开了disableDepthTestDistance: Infinity在旋转相机时还会触发大量深度测试计算低端设备上表现为“卡死”。这就是上一章说的改用Primitive批量标记能直接规避掉。瓦片错乱的另一个常见原因是缓存污染。浏览器或Cesium自带的TileCache缓存了旧瓦片而天地图更新了影像版本新旧版本混在一起显示会产生割裂。排查时直接在Cesium.Viewer构造参数里设置imageryLayer的缓存规则或清理浏览器缓存对比验证。还有一个隐蔽问题你用UrlTemplateImageryProvider时忘了加minimumLevel和maximumLevelCesium会在高缩放级别请求天地图不存在的瓦片返回异常数据导致部分区域白屏。天地图影像最高到18级所以maximumLevel: 18这个参数必须写。6. 坐标拾取、精度校验和数据预处理标记位置数据从哪来这直接关系到业务能不能用。这里结合“天地图坐标拾取”来聊。天地图官网提供了在线坐标拾取工具打开后在地图上点一下就能看到经纬度坐标和对应的级别。很多非GIS背景的业务人员都用这个工具来提供点位数据拿到的是CGCS2000坐标直接落到Cesium里没问题。但这种方式精度只到小数点后4位左右约10米对河流断面、排污口这种需要精确位置的对象不够用。项目里我推荐的做法是在Cesium画布上加一个“勘察标绘模式”让业务人员直接在三维地球上点选位置生成标记而不是线下用坐标拾取工具抄数据再录系统。实现起来不复杂const pickHandler new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); function enablePickMode(callback) { pickHandler.setInputAction((movement) { // 用椭球表面拾取忽略已有的标记和模型 const ray viewer.camera.getPickRay(movement.position); const cartesian viewer.scene.globe.pick(ray, viewer.scene); if (cartesian) { const carto Cesium.Cartographic.fromCartesian(cartesian); const lng Cesium.Math.toDegrees(carto.longitude); const lat Cesium.Math.toDegrees(carto.latitude); callback({ lng, lat, height: carto.height }); } }, Cesium.ScreenSpaceEventType.LEFT_CLICK); }这样采到的点位是真正贴合地形的三维坐标高度也有值后面叠加水位标尺、淹没分析都方便。再说精度校验。外部系统导出的POI数据经常有坐标漂移的情况尤其是GCJ02坐标直接拿来用位置会偏移几百米。比对方法很简单拿一个硬件GPS终端去现场采几个已知点把这几个点在Cesium里和天地图影像对照偏差明显的就说明数据坐标系不对需要写个转换工具批量清洗。我给团队定过一个规矩所有从外部系统导入的点位必须经过“坐标转换—剔除异常—现场抽检”三道关卡才能入库否则做出来的标记系统就是“看着有数据到现场根本不在地儿上”。7. 一套完整的业务标记方案结合实际项目串起来为了更好理解我把前面零零散散的内容组装成一个模拟场景河长制巡河系统。需求是加载河道的倾斜摄影、显示各级河长公示牌和监测站点点击标记能看到实时水位和巡河记录。第一步底图配置。加载天地图影像和注记外加Cesium官方地形。第二步加载倾斜摄影模型const tileset await Cesium.Cesium3DTileset.fromUrl(/data/3dtiles/river_3d/tileset.json); viewer.scene.primitives.add(tileset); try { await viewer.scene.viewer.flyTo(tileset); } catch (e) { // 首次加载相机飞到某个默认河段视角 }第三步加载标记数据。业务数据是后端接口返回的JSON数组包含点位名称、类型、经纬度、状态等。用MarkerLayer批量添加不同类型用不同图标雨量站蓝色水滴、水位站绿色标尺、公示牌黄色底牌。然后对BillboardCollection注册点击事件const markerLayer new MarkerLayer(viewer); markerLayer.addMarkers(bizPoints); const clickHandler new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); clickHandler.setInputAction((movement) { const picked viewer.scene.pick(movement.position); if (Cesium.defined(picked) picked.primitive instanceof Cesium.BillboardCollection) { const attrs picked.id; // attrs里是addMarkers时传的attributes openInfoWindow(attrs, movement.position); } }, Cesium.ScreenSpaceEventType.LEFT_CLICK);第四步信息窗。Cesium自带的信息框比较简陋我通常是直接在HTML里划一个绝对定位的div点击标记时把该点位的详情渲染进去再用Cesium.SceneTransforms.worldToWindowCoordinates把三维坐标转成屏幕坐标来定位弹窗function openInfoWindow(attrs, screenPos) { infoPanel.style.left screenPos.x 12 px; infoPanel.style.top screenPos.y 12 px; infoPanel.style.display block; infoPanel.innerHTML buildHtmlByType(attrs); }这就是一个能上生产的最小完整方案。8. 进阶方向标注聚合、热力图、鹰眼和单体化的组合标记系统跑通之后还会有各种进阶需求找上门。这里挑几个高频场景说一下。8.1 海量点聚合当标记数量过万直接渲染还是会有压力。天地图热词里出现了“百度地图同一图层大量数据标记效果”本质就是聚类聚合。Cesium侧有对应的做法在GeoJSON数据源上开启聚类const dataSource await Cesium.GeoJsonDataSource.load(/data/points_10w.json); const clustering dataSource.clustering; clustering.enabled true; clustering.pixelRange 35; clustering.minimumClusterSize 3; clustering.clusterEvent.addEventListener((clusteredEntities, cluster) { cluster.label.text clusteredEntities.length.toString(); cluster.billboard.image /markers/cluster.png; });这种方式和前面的BillboardCollection思路互补数据少用Primitive集合数据特别大用数据源聚类。8.2 标记热力图热力图可以和标记联动。比如排污口监测数据分布用色块表达浓度高低。通常做法是先把标记点位的值转成屏幕坐标在Canvas上绘制热力底图然后作为图片叠加到Cesium的Rectangle上。第三方的heatmap.js配合Cesium转换函数可以做到// 伪代码示意 const heatCanvas renderHeatMap(heatData); // 生成canvas viewer.entities.add({ rectangle: { coordinates: Cesium.Rectangle.fromDegrees(west, south, east, north), material: heatCanvas, // 关闭深度测试保证热力显示在模型上层 } });这个方案在河湖污染扩散趋势展示里很实用。8.3 鹰眼图联动“Cesium鹰眼”本质是一个缩略版地图同步视图。在主Cesium实例的camera.changed事件里更新缩略地球的相机位置反过来在缩略地球上点击拖动同步控制主视图。很多项目中鹰眼图直接用一个Canvas画主视图覆盖范围的矩形框就行不需要再加载一套完整天地图。8.4 单体化与标记的深度结合单体化不只是“点了弹窗”。我在做巡检业务时把单体化的楼层标记和业务流关联了起来点击楼栋单体化模型→弹出该栋楼的标记列表→点击某个标记→视角平滑飞到标记点位并打开摄像头画面。这套互动在数字孪生、智慧园区项目里是标配。实现核心在于“定位飞行”的体验function flyToMarker(entity) { const pos entity.position.getValue(viewer.clock.currentTime); viewer.camera.flyTo({ destination: pos, orientation: { heading: Cesium.Math.toRadians(20), pitch: Cesium.Math.toRadians(-45), roll: 0 }, duration: 1.5 }); }9. 项目里的几个“如果不注意必踩”的细节说几个在开发中反复被问到的坑集中排除一遍。第一个是天地图tk泄漏风险。tk写在前端等于公开了别人拿到就能用你的配额。稍微大一点的项目建议后端做一层代理前端请求同源接口由后端转发天地图WMTS并把tk放在服务端配置里。这样既保护了配额又能统一做缓存和限流。第二个是标记图标切图规范。别小看图标尺寸和锚点。我见过图标画得很大但视觉重心和经纬度位置偏差很远放大后点不准。建议所有标记图标统一为32x32或48x48锚点固定在底部中心verticalOrigin: BOTTOM并且图标主体视觉重心校准在锚点上方2~4像素处这样最符合手指/鼠标点击习惯。第三个是高空视野下的标记符号缩放。默认Billboard在缩放时字号和图标尺寸不变导致拉高视野后挤成一团。可结合实际需求在camera.changed事件里根据相机高度动态调整scaleByDistance或切换聚合图层billboard.scaleByDistance new Cesium.NearFarScalar(1000, 1.0, 50000, 0.4);第四个是设备和浏览器兼容性。Cesium开发多数时候跑在Chrome上没问题但业务现场可能用老旧Windows机器上的浏览器WebGL版本低会白屏或卡顿。项目交付时我会额外注意目标浏览器必须支持WebGL2GPU加速没被禁用低端机器上尽量关掉抗锯齿、降低分辨率系数天地图影像层和模型数量做取舍不支持一键全开。第五个是关于“三维地球滚动出现崩溃”这类问题的调试思路。不要急着改代码先把浏览器控制台的报错、网络请求失败、WebGL警告一起截图记录再对比正常环境的表现。很多时候是某台机器的显卡驱动问题不是代码Bug。遇到这种问题优先降级Cesium的contextOptions关闭一些高级渲染特性测试看是否复现。第六个是移动端适配。热词里有“天地图移动端uniapp能用吗”如果你打算在移动端内嵌Cesium需要确认天地图服务是否允许在移动App内访问。用WebView加载H5页面问题不大但要注意触摸交互和性能且天地图的tk对Referer有校验移动端请求时Referer可能为空需要后端代理配合。10. 我最后想说的两句话做Cesium开发久了会发现真正拉开项目差距的往往不是炫酷效果而是底图稳不稳、标记准不准、数据能不能和业务对上。天地图给了你一个合规、稳定、免费的底子剩下的就是把标记这个基本功做扎实。Entity爽一时Primitive稳一世单体化数据协议早定早省心。回到标题“02-Cesium-实现天地图标记”如果你要做的是简单Demo照着第2章和第3章的代码就能出效果。如果你要做的是能上生产的业务系统至少要把第7章的完整串法和第9章的避坑清单过一遍。踩过这一轮你手上的Cesium项目基本就不会再被“底图和标记”拖后腿了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑