ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Cesium与SuperMap三维智慧城市落地实战:架构、对接与避坑指南

Cesium与SuperMap三维智慧城市落地实战:架构、对接与避坑指南

简介:这是一套面向三维WebGIS开发者与智慧城市项目实践者的Cesium三维可视化工程,兼容超图SuperMap在线三维服务,适合具备一定前端与WebGL基础、希望快速搭建三维城市场景的读者参考。工程内含智慧城市、警情模拟、水积警情模拟三个独立场景,每个HTML对应一个可运行示例,可直接在VSCode或HBuilder中打开调试,所有3D模型均已内置,无需额外下载。压缩包共约2000个文件,涵盖png、jpg贴图,js脚本,css样式,json配置,以及s3m、gltf、glb等三维模型与地形、KML数据,整体约202.4MB,目录中Build、examples与city系列脚本分工明确,便于按模块查阅。目前已有2355人学习下载,读者可借此掌握Cesium场景搭建、超图服务对接与警情、积水等专题模拟的实现思路,快速复用到自己的三维城市项目中。

1. 三维智慧城市项目:从 Cesium 到 SuperMap 的完整落地路径

很多做三维 WebGIS 的开发者都遇到过这个场景:客户要求大屏上跑一个城市级三维场景,建筑白模、路网、水系、管线全都要,还得能对接已有的 GIS 平台服务。你打开 Cesium 官方示例,加载个全球地形没问题,但一旦换成国内某测绘院提供的三维服务地址,要么跨域,要么坐标系对不上,要么瓦片层级错乱。这个项目就是冲着这类需求去的——基于 Cesium 构建一套三维智慧城市系统,同时兼容 SuperMap 的三维服务接口。它适合两类人:一是手里有三维数据、需要快速搭出可视化平台的 GIS 开发;二是想学 Cesium 与国内 GIS 平台对接的 WebGL 前端。源码结构清晰,跑起来之后能直接看到城市级三维场景,改起来也有明确的模块边界。

2. 技术选型与架构拆解:为什么是 Cesium 加 SuperMap 的组合

2.1 三维渲染层为什么锁定 Cesium

Cesium 在三维 WebGL 领域的位置不用多说,真正让这个项目选它的原因有三个。第一,它对 3D Tiles 的支持最成熟,城市级建筑白模动辄几十万面片,用 3D Tiles 分块加载才能保证帧率。第二,Cesium 的相机控制和场景图 API 足够开放,智慧城市常见的飞行漫游、图层显隐、点击查询都能直接操作 Viewer 和 Scene 对象。第三,它原生支持 WGS84 和 Web Mercator 两套坐标系,和国内 GIS 数据对接时少一层转换。

项目里 Cesium 的引入方式不是 npm 包,而是静态资源目录下的构建版本。这样做的好处是部署简单,不需要前端构建工具也能跑;代价是版本升级要手动替换。常见做法是在index.html里通过script标签引入Cesium.js,然后设置window.CESIUM_BASE_URL指向Build/Cesium目录,否则 Worker 和 Assets 加载会 404。

<!-- index.html 中 Cesium 的引入方式 --> <script src="./Cesium/Cesium.js"></script> <script> // 必须设置,否则 Cesium 内部的 Worker、Assets 路径会解析错误 window.CESIUM_BASE_URL = './Cesium/'; </script>

这里的关键参数是CESIUM_BASE_URL,它决定了 Cesium 运行时去哪里找Workers、Assets、Widgets这些目录。如果你用 webpack 打包,通常用copy-webpack-plugin把这些目录拷到输出目录,再配合DefinePlugin注入这个变量。项目里直接静态引入,省去了构建配置,适合快速验证和二次开发。

2.2 SuperMap 三维服务的对接方式

SuperMap 的三维服务通常以 iServer 为发布端,提供 REST 风格的三维瓦片、地形、模型服务。Cesium 本身不认识 SuperMap 的私有协议,所以项目里做了一层适配。核心思路是:把 SuperMap 的三维服务地址当作 Cesium 的UrlTemplateImageryProvider或Cesium3DTileset的数据源,通过重写requestImage或直接拼接瓦片 URL 来加载。

// 对接 SuperMap 三维瓦片服务的典型写法 const superMapProvider = new Cesium.UrlTemplateImageryProvider({ // SuperMap iServer 三维瓦片服务地址模板 url: 'http://localhost:8090/iserver/services/3D-City/rest/realspace/datas/city/tiles/{z}/{x}/{y}.png', // 指定坐标系,SuperMap 常用 4326 或 3857 tilingScheme: new Cesium.GeographicTilingScheme(), // 最大层级要和 SuperMap 发布时一致,否则会出现瓦片错位 maximumLevel: 18, // 关闭 Cesium 默认的瓦片裁剪,避免边缘黑边 rectangle: Cesium.Rectangle.fromDegrees(116.0, 39.0, 117.0, 40.0) }); viewer.imageryLayers.addImageryProvider(superMapProvider);

这段代码里tilingScheme和maximumLevel是两个最容易翻车的参数。SuperMap 发布三维服务时如果用的是地理坐标系,这里就必须用GeographicTilingScheme;如果发布的是投影坐标系,得换成WebMercatorTilingScheme。maximumLevel对不上,瓦片要么加载不出来,要么层级混乱。rectangle参数用来限定加载范围,城市级项目不加这个,Cesium 会尝试请求全球瓦片,直接拖垮浏览器。

2.3 整体架构分层

项目在结构上分了三层:数据服务层、三维渲染层、业务交互层。数据服务层负责和 SuperMap iServer 通信,获取三维瓦片、地形、属性数据;三维渲染层就是 Cesium Viewer 和场景管理;业务交互层处理点击查询、图层控制、飞行定位这些操作。

目录结构大致如下:

project/ ├── Cesium/ # Cesium 静态资源 ├── src/ │ ├── core/ # 核心初始化逻辑 │ │ ├── viewer.js # Viewer 创建与配置 │ │ └── layerManager.js # 图层管理 │ ├── services/ # 服务对接 │ │ ├── supermap.js # SuperMap 服务封装 │ │ └── terrain.js # 地形服务 │ ├── interaction/ # 交互逻辑 │ │ ├── pick.js # 点击拾取 │ │ └── flyTo.js # 飞行定位 │ └── main.js # 入口 ├── index.html └── assets/ # 图标、样式

这种分层的好处是,换掉 SuperMap 换成别的 GIS 平台,只需要改services/目录;换掉 Cesium 换成别的渲染引擎,只需要改core/和interaction/。对于二次开发来说,边界清晰,不会牵一发动全身。

3. 跑起来:环境准备与三维场景初始化

3.1 本地环境与依赖检查

项目本身是纯前端静态资源,不需要 Node.js 构建,但需要一个本地 HTTP 服务来跑,因为 Cesium 的 Worker 和三维瓦片请求都不支持file://协议。最省事的方式是用 Python 自带的 HTTP 服务:

# 在项目根目录执行,默认监听 8000 端口 python -m http.server 8000

如果你机器上有 Node.js,也可以用npx serve或http-server。注意端口不要和 SuperMap iServer 冲突,iServer 默认是 8090。启动后浏览器访问http://localhost:8000,如果页面白屏,先打开控制台看有没有CESIUM_BASE_URL相关的 404。

提示:Cesium 的静态资源目录必须和CESIUM_BASE_URL指向一致,否则会出现「Viewer 初始化了但地球不显示」的情况。

3.2 Viewer 初始化与场景配置

Viewer 是整个三维场景的容器,项目里对它的配置做了封装,方便统一管理。

// src/core/viewer.js export function createViewer(containerId) { const viewer = new Cesium.Viewer(containerId, { // 关闭不需要的默认控件,减少干扰 animation: false, timeline: false, baseLayerPicker: false, geocoder: false, homeButton: false, sceneModePicker: false, navigationHelpButton: false, // 使用自定义影像,不用 Cesium 默认的 Bing 地图 imageryProvider: false, // 开启地形,但地形服务地址在外部注入 terrainProvider: new Cesium.EllipsoidTerrainProvider(), // 关闭抗锯齿,城市级场景下性能优先 contextOptions: { webgl: { antialias: false } } }); // 隐藏 Cesium 默认的版权信息条 viewer.cesiumWidget.creditContainer.style.display = 'none'; // 开启深度检测,保证建筑和地形的遮挡关系正确 viewer.scene.globe.depthTestAgainstTerrain = true; // 设置初始视角,定位到城市范围 viewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees(116.5, 39.9, 5000), orientation: { heading: Cesium.Math.toRadians(0), pitch: Cesium.Math.toRadians(-45), roll: 0 } }); return viewer; }

这里有几个参数值得展开。imageryProvider: false是为了避免 Cesium 默认去请求 Bing 地图,国内网络环境下大概率超时。depthTestAgainstTerrain开启后,建筑模型会被地形正确遮挡,但代价是渲染开销增加,如果场景里地形起伏不大,可以关掉换性能。contextOptions.webgl.antialias关掉抗锯齿,在 4K 大屏上可能有点锯齿,但帧率能提升不少,智慧城市大屏通常更看重流畅度。

3.3 加载 SuperMap 三维服务与地形

Viewer 创建好之后,下一步是把 SuperMap 的三维瓦片和地形挂上去。项目里把这一步单独放在services/supermap.js中。

// src/services/supermap.js export function addSuperMap3DLayer(viewer, options) { const { url, maximumLevel, rectangle } = options; // 三维瓦片服务 const imageryProvider = new Cesium.UrlTemplateImageryProvider({ url: url, tilingScheme: new Cesium.GeographicTilingScheme(), maximumLevel: maximumLevel || 18, rectangle: rectangle ? Cesium.Rectangle.fromDegrees(...rectangle) : undefined }); viewer.imageryLayers.addImageryProvider(imageryProvider); // 如果 SuperMap 同时发布了地形服务,用 CesiumTerrainProvider 加载 if (options.terrainUrl) { viewer.terrainProvider = new Cesium.CesiumTerrainProvider({ url: options.terrainUrl, // 请求地形顶点法线,用于光照计算 requestVertexNormals: true }); } return imageryProvider; }

调用的时候传入 SuperMap iServer 发布出来的服务地址。requestVertexNormals这个参数如果地形服务支持,开启后建筑和地形的光照会更自然,但会增加数据传输量。城市级项目里如果地形起伏不大,可以关掉。

加载完成后,如果发现瓦片位置偏移,优先检查tilingScheme和 SuperMap 发布时选的坐标系是否一致。这是最常见的对接问题,没有之一。

4. 核心模块拆解:图层管理、点击拾取与飞行定位

4.1 图层管理器的设计与实现

智慧城市项目里图层数量多,建筑、道路、水系、管线、标注,每个都要能独立控制显隐和顺序。项目里用layerManager统一管理。

// src/core/layerManager.js export class LayerManager { constructor(viewer) { this.viewer = viewer; this.layers = new Map(); // key: layerId, value: { type, layer } } // 添加影像图层 addImageryLayer(id, provider) { const layer = this.viewer.imageryLayers.addImageryProvider(provider); this.layers.set(id, { type: 'imagery', layer }); return layer; } // 添加 3D Tiles 图层 async add3DTilesLayer(id, url) { const tileset = await Cesium.Cesium3DTileset.fromUrl(url, { // 最大屏幕空间误差,值越小越精细,性能开销越大 maximumScreenSpaceError: 16, // 开启动态屏幕空间误差,根据帧率自动调整 dynamicScreenSpaceError: true, // 跳过细节层次,加速加载 skipLevelOfDetail: true }); this.viewer.scene.primitives.add(tileset); this.layers.set(id, { type: '3dtiles', layer: tileset }); return tileset; } // 控制显隐 setVisible(id, visible) { const item = this.layers.get(id); if (!item) return; if (item.type === 'imagery') { item.layer.show = visible; } else { item.layer.show = visible; } } // 移除图层 remove(id) { const item = this.layers.get(id); if (!item) return; if (item.type === 'imagery') { this.viewer.imageryLayers.remove(item.layer); } else { this.viewer.scene.primitives.remove(item.layer); } this.layers.delete(id); } }

maximumScreenSpaceError是 3D Tiles 加载的核心参数,默认 16,调小到 8 会更精细但帧率下降明显,调到 32 适合大屏远观。dynamicScreenSpaceError开启后 Cesium 会根据当前帧率动态调整这个值,城市级场景建议开启。skipLevelOfDetail在快速移动相机时能跳过中间层级,减少卡顿,但静止时可能看到模型突然变精细,看项目取舍。

4.2 点击拾取与属性查询

点击建筑弹出属性面板是智慧城市的标配功能。Cesium 提供了Scene.pick方法,但直接拿到的对象需要区分是 3D Tiles 还是普通实体。

// src/interaction/pick.js export function initPick(viewer, onPick) { const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); handler.setInputAction((movement) => { // 获取点击位置的拾取对象 const picked = viewer.scene.pick(movement.position); if (!picked) { onPick(null); return; } // 3D Tiles 的拾取结果包含 getProperty 方法 if (picked instanceof Cesium.Cesium3DTileFeature) { const properties = {}; const propertyIds = picked.getPropertyIds(); propertyIds.forEach((id) => { properties[id] = picked.getProperty(id); }); onPick({ type: '3dtiles', properties }); return; } // 普通 Entity 的拾取结果 if (picked.id instanceof Cesium.Entity) { onPick({ type: 'entity', entity: picked.id }); return; } onPick(null); }, Cesium.ScreenSpaceEventType.LEFT_CLICK); }

这里的关键是区分Cesium3DTileFeature和Entity。3D Tiles 的属性通过getProperty逐个读取,Entity 则直接拿id。实际项目里,建筑白模通常用 3D Tiles,属性存在瓦片的 batch table 里;而标注、点位用 Entity 更灵活。ScreenSpaceEventHandler记得在组件销毁时调用handler.destroy(),否则多次初始化会导致点击事件重复触发。

4.3 飞行定位与相机控制

飞行定位用于从列表点击某个建筑后,相机平滑飞到目标位置。

// src/interaction/flyTo.js export function flyToFeature(viewer, target) { // target 可以是 Entity、3D Tiles 的包围盒中心,或经纬度坐标 if (target instanceof Cesium.Entity) { viewer.flyTo(target, { duration: 2, // 飞行时间,秒 offset: new Cesium.HeadingPitchRange( 0, Cesium.Math.toRadians(-30), 500 // 距离目标 500 米 ) }); return; } // 如果是经纬度坐标 viewer.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees( target.longitude, target.latitude, target.height || 1000 ), orientation: { heading: Cesium.Math.toRadians(target.heading || 0), pitch: Cesium.Math.toRadians(target.pitch || -45), roll: 0 }, duration: 2 }); }

offset里的HeadingPitchRange控制相机相对目标的方位角和俯仰角,range是距离。飞行时间duration设成 2 秒比较自然,太短会晕,太长用户等不及。如果目标是一个 3D Tileset,flyTo会自动计算包围盒,但有时候包围盒太大,飞过去之后建筑很小,这时候需要手动指定offset的range。

5. 避坑与排查:对接 SuperMap 三维服务时最容易翻车的几个点

5.1 瓦片错位或层级混乱

现象:加载 SuperMap 三维瓦片后,地图位置整体偏移,或者放大到某一层级后瓦片突然错乱。

原因:tilingScheme和 SuperMap 发布服务时选的坐标系不一致。SuperMap iServer 发布三维服务时,如果数据是地理坐标系(如 CGCS2000),必须用GeographicTilingScheme;如果是投影坐标系(如 Web Mercator),必须用WebMercatorTilingScheme。另外maximumLevel如果大于 SuperMap 实际发布的层级,Cesium 会请求不存在的瓦片,导致空白或错位。

解决:先确认 SuperMap 服务的坐标系和最大层级,再对应设置 Cesium 的参数。可以在浏览器开发者工具的网络面板里看瓦片请求的 URL,如果返回 404 或图片尺寸不对,基本就是这两个参数的问题。

5.2 跨域请求被拦截

现象:控制台报CORS policy错误,瓦片和地形都加载不出来。

原因:SuperMap iServer 默认没有开启跨域头,而 Cesium 的请求是跨域请求。

解决:在 iServer 的配置里开启 CORS,或者在前端加一层代理。开发阶段可以用http-server的--cors参数,生产环境建议在 Nginx 里配置Access-Control-Allow-Origin。注意不要用*,指定具体域名更安全。

5.3 地形与建筑穿插

现象:建筑模型一半埋在地形下面,一半露出来。

原因:depthTestAgainstTerrain没有开启,或者地形服务和建筑模型的坐标系不一致。

解决:开启viewer.scene.globe.depthTestAgainstTerrain = true。如果开启后仍然穿插,检查地形服务和建筑模型是否用了同一套坐标系。常见做法是地形用 SuperMap 发布的地形服务,建筑用 3D Tiles,两者都基于同一坐标系发布。

5.4 内存泄漏与帧率下降

现象:运行一段时间后浏览器内存飙升,帧率从 60 掉到 20 以下。

原因:图层反复添加移除没有释放,或者 3D Tiles 的缓存没有清理。

解决:移除图层时调用viewer.scene.primitives.remove并置空引用;3D Tiles 可以设置tileset.maximumMemoryUsage限制内存占用,默认是 512MB,城市级项目可以调到 1024MB 或更高,但要注意机器配置。另外ScreenSpaceEventHandler在页面切换时一定要destroy。

5.5 飞行定位后相机失控

现象:调用flyTo后相机飞到了奇怪的位置,或者飞行结束后无法手动控制。

原因:flyTo的offset参数设置不当,或者飞行过程中用户手动操作了相机导致冲突。

解决:飞行前先viewer.camera.cancelFlight(),飞行时给duration设一个合理值,不要设成 0 或负数。如果飞行后相机无法控制,检查是否有其他代码在持续修改相机位置,比如自动旋转或漫游逻辑没有正确停止。

6. 二次开发与改造:从能跑到好用的几个关键动作

项目跑起来之后,二次开发的空间主要在三个方向:数据源替换、交互增强、性能调优。

数据源替换是最常见的需求。如果客户用的是另一家 GIS 平台的三维服务,只需要改services/supermap.js里的 URL 拼接逻辑和tilingScheme配置。我一般会把这部分抽成一个适配器接口,不同平台实现不同的getImageryProvider和getTerrainProvider方法,上层业务代码不用动。

// 适配器模式的改造思路 class GISServiceAdapter { getImageryProvider(options) { throw new Error('必须实现 getImageryProvider'); } getTerrainProvider(options) { throw new Error('必须实现 getTerrainProvider'); } } class SuperMapAdapter extends GISServiceAdapter { getImageryProvider(options) { return new Cesium.UrlTemplateImageryProvider({ url: options.url, tilingScheme: new Cesium.GeographicTilingScheme(), maximumLevel: options.maximumLevel }); } getTerrainProvider(options) { return new Cesium.CesiumTerrainProvider({ url: options.terrainUrl, requestVertexNormals: true }); } }

这样换平台的时候,只需要新增一个XXXAdapter,在入口处切换实例即可。验证方法也很直接:把SuperMapAdapter换成新的适配器,跑一遍加载、点击、飞行三个核心流程,看有没有报错和位置偏移。

交互增强方面,项目目前只有点击拾取和飞行定位。实际智慧城市项目通常还需要框选查询、图层叠加分析、动态效果(如管线流动、预警闪烁)。这些都可以在interaction/目录下新增模块,通过事件总线或回调函数和主流程解耦。我一般会用一个简单的发布订阅模式,避免模块之间直接引用。

性能调优是城市级项目的必修课。除了前面提到的maximumScreenSpaceError和maximumMemoryUsage,还可以做几件事:一是用Cesium.RequestScheduler限制并发请求数,避免一次性请求太多瓦片;二是对远处的建筑做裁剪,只加载相机视野范围内的 3D Tiles;三是把不常看的图层做成按需加载,而不是一开始就全部挂上去。

// 限制并发请求数,避免浏览器被瓦片请求打满 Cesium.RequestScheduler.maximumRequests = 50; Cesium.RequestScheduler.maximumRequestsPerServer = 20;

这两个参数在瓦片服务响应慢的时候特别有用,默认值偏高,城市级场景下调小一点能明显改善卡顿。

从那以后我每次对接新的三维服务,都强制先跑一遍「坐标系确认 → 层级对齐 → 跨域检查 → 小范围加载」这四步,确认没问题再全量挂图层。很多翻车都是因为跳过了其中某一步,等到场景全加载出来才发现问题,排查成本翻倍。希望这些经验能帮到你,源码跑起来之后,先从替换一个图层开始改,比一上来就动核心逻辑要稳得多。

本文还有配套的精品资源,点击获取

返回列表