简介:本资源面向GIS开发与三维可视化方向的开发者,提供一套将SuperMap与Cesium结合的3D地图展示方案,重点解决倾斜摄影测量数据在Web端的三维加载与交互问题。项目以Vue.js为前端框架,借助Babel完成语法转换,并通过SuperMap-Cesium-3D插件把SuperMap的空间数据管理能力与Cesium的WebGL渲染能力整合,适合具备一定前端与GIS基础、希望快速搭建三维场景的读者参考。压缩包共560个文件,约25.84MB,以png、jpg图片与js脚本为主,辅以json、css、vue、html等前端资源,另含wasm、terrain、s3m等三维数据与地形文件,以及browserslistrc、vue.config.js、babel.config.js、package.json等工程配置,目录结构完整。目前已有725人学习下载,可帮助读者理解三维地图项目的依赖组织、多浏览器兼容配置与倾斜摄影集成思路。
1. 倾斜摄影模型在三维地图里加载失败,问题往往出在哪
很多做三维 GIS 的同行第一次接触 superMap-Cesium-3D 显示地图、倾斜摄影这条链路时,都会遇到一个反直觉的现象:数据明明在桌面端能正常打开,OSGB 目录也完整,可一旦放进 Cesium 场景,要么整片模型不出现,要么只冒出一个灰白色方块,控制台还安静得像个黑匣子。这不是玄学,八成是坐标系、根节点位置或者服务发布方式三者之一没对齐。
superMap-Cesium-3D 本质是把 SuperMap iServer 或 iClient3D 的三维能力挂到 Cesium 的渲染管线上,倾斜摄影则是用无人机多角度采集、经空三加密和纹理映射生成的实景三维模型,常见格式是 OSGB 加一个 metadata.xml。两者结合要解决的核心问题只有一个:让 Cesium 知道模型在哪、朝哪、多大。适合谁看?做智慧园区、城市三维底图、工程进度可视化的前端和 GIS 工程师,只要你能跑起一个静态页面,就能跟着往下走。
2. 从 OSGB 到 3D Tiles:倾斜摄影进 Cesium 的完整链路
2.1 为什么不能直接把 OSGB 丢给 Cesium
Cesium 原生只认 3D Tiles,不认 OSGB。OSGB 是 OpenSceneGraph 的二进制场景格式,一棵树状节点结构,每个节点带自己的变换矩阵和纹理引用。Cesium 的渲染循环没法直接遍历这种结构,所以中间必须做一次格式转换,把 OSGB 的节点树压平成 3D Tiles 的 tileset.json 加一批 b3dm 或 pnts 瓦片。
常见做法有两种。第一种是用 SuperMap iDesktopX 直接导入 OSGB,再导出为 3D Tiles 或 SuperMap 自己的 S3M 缓存。第二种是用开源工具链,比如把 OSGB 转成 glTF 再打包成 3D Tiles。我一般会优先走 iDesktopX,因为坐标系配准和 LOD 分层它已经帮你处理好了,省掉大量手工调参。
这里有个关键概念叫根节点变换。倾斜摄影生成时,模型原点往往在测区某个角点,甚至直接是投影坐标的原始值,比如东坐标 500000、北坐标 3000000 这种量级。Cesium 场景里如果直接把这个坐标当局部坐标用,模型会飞到地球外面去。所以转换工具会生成一个 region 或 transform,把模型整体平移到 Cesium 的 ECEF 坐标系下。
2.2 用 iDesktopX 完成 OSGB 到 3D Tiles 的转换
下面这套流程是我在多个模拟项目里反复用过的,参数按默认走基本能出结果,但有几个地方必须改。
第一步,新建一个球面场景,坐标系选 WGS84。不要选平面场景,倾斜摄影最终要贴在地球曲面上。
第二步,导入 OSGB 数据。在 iDesktopX 里选择「三维数据」→「倾斜摄影」→「导入 OSGB」,指向包含 metadata.xml 的目录。
# 假设 OSGB 数据在 /data/uav/osgb/ 下 # metadata.xml 同级目录应有 Data 文件夹 /data/uav/osgb/ ├── metadata.xml └── Data/ ├── Tile_+000_+000/ │ ├── Tile_+000_+000.osgb │ └── ... └── ...导入时注意 SRS 设置。如果 metadata.xml 里已经写了 EPSG 码,iDesktopX 会自动识别;如果没写,你得手动指定,通常是 EPSG:4326 或 EPSG:4547 这类地方坐标系。选错坐标系,模型会整体偏移几百米,而且旋转角度也不对。
第三步,生成 S3M 缓存或 3D Tiles。右键数据集选择「生成缓存」,参数如下表。
| 参数 | 建议值 | 说明 |
|---|---|---|
| 缓存类型 | S3M 3.0 或 3D Tiles | 要挂 Cesium 选 3D Tiles |
| 瓦片边长 | 128 或 256 | 越小越精细,但瓦片数量翻倍 |
| 纹理压缩 | WebP 或 JPEG | WebP 体积小,但老浏览器要 polyfill |
| LOD 层级 | 自动 | 手动设容易断层 |
| 线程数 | CPU 核数一半 | 全开容易内存溢出 |
生成完成后,你会得到一个 tileset.json 和一堆 b3dm 文件。这个 tileset.json 就是 Cesium 要加载的入口。
2.3 在 Cesium 里加载 3D Tiles 并叠加 SuperMap 底图
SuperMap-Cesium-3D 的加载方式和原生 Cesium 几乎一样,区别在于底图服务地址和认证参数。下面是一个最小可运行示例。
// 初始化 Cesium Viewer,关闭默认影像和地形 const viewer = new Cesium.Viewer('cesiumContainer', { imageryProvider: false, // 先不挂默认底图 terrainProvider: false, // 地形后续单独加 baseLayerPicker: false, geocoder: false, homeButton: false, sceneModePicker: false, navigationHelpButton: false, animation: false, timeline: false }); // 挂载 SuperMap iServer 发布的影像底图 const imageryProvider = new Cesium.SuperMapImageryProvider({ url: 'http://localhost:8090/iserver/services/map-world/rest/maps/World', // 如果是加密服务,这里加 credential // credential: new Cesium.Credential('token', 'your-token') }); viewer.imageryLayers.addImageryProvider(imageryProvider); // 加载倾斜摄影 3D Tiles const tileset = viewer.scene.primitives.add( new Cesium.Cesium3DTileset({ url: './tileset.json', // 转换生成的入口文件 maximumScreenSpaceError: 16, // 默认 16,调小更清晰但更卡 maximumNumberOfLoadedTiles: 1000, skipLevelOfDetail: true, // 开启 LOD 跳过,提升加载速度 baseScreenSpaceError: 1024, skipScreenSpaceErrorFactor: 16, skipLevels: 1, immediatelyLoadDesiredLevelOfDetail: false, loadSiblings: false, cullWithChildrenBounds: true }) ); // 等 tileset 就绪后飞到模型位置 tileset.readyPromise.then(function (tileset) { viewer.zoomTo(tileset, new Cesium.HeadingPitchRange( 0, Cesium.Math.toRadians(-30), 500 )); }).otherwise(function (error) { console.error('3D Tiles 加载失败:', error); });这段代码的逻辑说明:先创建一个不带默认底图的 Viewer,避免网络请求拖慢首屏;然后挂 SuperMap 的影像服务,注意 URL 里的服务名和地图名要和你 iServer 上发布的一致;最后加载 3D Tiles,maximumScreenSpaceError 是核心参数,16 是平衡值,调到 8 模型更精细但显存占用翻倍,调到 32 会明显糊。
参数说明:skipLevelOfDetail 设为 true 后,Cesium 会跳过中间层级直接加载目标层级,大场景首屏快很多,但快速旋转视角时可能出现短暂空洞。如果你的场景对完整性要求高,把它设成 false,代价是加载时间变长。
2.4 坐标系配准:模型偏移和旋转的根因
倾斜摄影最常见的翻车现场就是模型位置对不上底图。原因通常有三个:一是 OSGB 生成时用的坐标系和 Cesium 底图不一致;二是转换时没有正确写入 transform;三是模型本身有旋转,但转换工具没识别。
排查方法:在 Cesium 里打开 tileset 的 boundingSphere,看中心点坐标是否落在你预期的经纬度附近。
tileset.readyPromise.then(function (tileset) { const center = tileset.boundingSphere.center; const cartographic = Cesium.Cartographic.fromCartesian(center); console.log('模型中心经度:', Cesium.Math.toDegrees(cartographic.longitude)); console.log('模型中心纬度:', Cesium.Math.toDegrees(cartographic.latitude)); console.log('模型中心高度:', cartographic.height); });如果经度纬度差了几度,说明坐标系没对上;如果高度是负几千,说明椭球高和海拔高搞混了。解决方式是在 iDesktopX 里重新指定坐标系并重新生成缓存,不要试图在 Cesium 里用 modelMatrix 硬掰,那样纹理和光照都会出问题。
3. 性能调优:让倾斜摄影在浏览器里跑顺的 4 个参数
3.1 maximumScreenSpaceError 到底怎么设
这个参数决定了一个瓦片在屏幕上占多少像素时才被加载。值越小,加载的瓦片越精细,显存和带宽消耗越大。经验值:桌面端 16,移动端 32,大屏展示 8。但这不是绝对的,如果你的模型纹理压缩率高,可以适当调小。
我一般会做一个自适应:根据设备像素比和屏幕宽度动态设。
const pixelRatio = window.devicePixelRatio || 1; const screenWidth = window.screen.width; let sse = 16; if (pixelRatio > 2 && screenWidth > 1920) { sse = 8; // 高分屏大屏,给足精度 } else if (screenWidth < 768) { sse = 32; // 移动端,保流畅 } const tileset = new Cesium.Cesium3DTileset({ url: './tileset.json', maximumScreenSpaceError: sse });3.2 纹理压缩与显存占用
倾斜摄影的纹理是大头,一个平方公里测区动辄几十 GB 纹理。转 3D Tiles 时如果纹理不压缩,浏览器直接爆显存。建议在 iDesktopX 生成缓存时选 WebP,压缩率比 JPEG 高 30% 左右,视觉损失很小。
如果已经生成了未压缩的缓存,可以用工具二次压缩,但要注意保留原始纹理的 mipmap 层级,否则远处瓦片会闪烁。
3.3 用 requestScheduler 控制并发请求数
Cesium 默认的并发请求数偏高,在弱网环境下反而拖慢首屏。可以手动限制。
viewer.scene.requestScheduler.maximumRequests = 6; viewer.scene.requestScheduler.maximumRequestsPerServer = 4;这两个值调小后,首屏瓦片加载更有序,不会出现一堆请求排队但一个都没完成的情况。代价是总加载时间可能略长,但用户体验更稳。
3.4 地形与倾斜摄影的贴合问题
如果场景里同时有地形和倾斜摄影,模型底部可能会悬空或陷入地面。原因是地形高程和模型高程基准不一致。解决办法是在 iDesktopX 里把模型底部裁剪到地形面,或者在 Cesium 里给 tileset 加一个高度偏移。
tileset.modelMatrix = Cesium.Matrix4.fromTranslation( new Cesium.Cartesian3(0, 0, -2.5) // 向下偏移 2.5 米 );这个偏移量要实测,不同测区不一样。我一般会先加载地形,再用鼠标拾取模型底部和地形面的高度差,取平均值。
4. 避坑指南:倾斜摄影加载的 5 个血泪教训
4.1 模型整体消失,控制台无报错
现象:页面空白,Network 里 tileset.json 请求 200,但 b3dm 一个都没加载。
原因:tileset.json 里的 geometricError 设得太大,Cesium 认为当前视角不需要加载任何瓦片。
解决:检查 tileset.json 根节点的 geometricError,通常应该在 500 左右。如果超过 10000,手动改小。或者用 viewer.zoomTo(tileset) 强制飞到模型位置。
4.2 模型出现但全是灰白色,没有纹理
现象:模型几何形状正确,但表面是纯色,没有照片纹理。
原因:b3dm 里的纹理引用路径不对,或者纹理格式浏览器不支持。
解决:打开一个 b3dm 文件,检查头部 JSON 里的 images 字段,看 uri 是否指向了实际存在的文件。如果是相对路径,确认和 tileset.json 的相对关系。另外,如果纹理是 DDS 格式,Cesium 默认不支持,要转成 WebP 或 JPEG。
4.3 旋转视角时模型闪烁或撕裂
现象:静止时正常,一旋转就出现瓦片闪烁、缝隙。
原因:LOD 切换时相邻瓦片的几何误差不一致,或者 skipLevelOfDetail 导致跳层加载。
解决:把 skipLevelOfDetail 设为 false,同时把 maximumScreenSpaceError 调大一点,让 LOD 切换不那么频繁。如果还不行,检查生成缓存时是否开启了「纹理压缩」但没开「几何压缩」,两者要配套。
4.4 加载到一半浏览器崩溃
现象:加载几十个瓦片后页面卡死,内存飙升。
原因:瓦片数量太多,或者单个瓦片太大。常见于瓦片边长设成 512 且没有做 LOD 分层的情况。
解决:重新生成缓存,瓦片边长降到 128 或 256,确保 LOD 层级至少 5 级。另外在 Cesium 里开启内存管理。
viewer.scene.globe.tileCacheSize = 100; // 默认 100,可适当调小 viewer.scene.primitives.destroyPrimitives = false; // 避免频繁销毁重建4.5 SuperMap 服务跨域导致底图不显示
现象:3D Tiles 正常,但 SuperMap 影像底图一片黑,控制台报 CORS 错误。
原因:iServer 默认没开跨域,或者端口不对。
解决:在 iServer 的 web.xml 里加 CORS 过滤器,或者用 Nginx 反向代理统一域名。临时方案是在浏览器启动参数里加 --disable-web-security,但仅限本地调试,不要带到生产。
5. 进阶技巧:用裁剪面和动态单体化做交互
倾斜摄影本身是一整块 mesh,没法直接点选单个建筑。要做交互,常见做法是动态单体化:在模型上方叠加一层矢量面,用面的 ID 关联属性,点击时通过 Cesium 的 pick 拿到面,再查属性表。
// 加载矢量面,作为单体化载体 const dataSource = new Cesium.CustomDataSource('building'); viewer.dataSources.add(dataSource); // 假设从 GeoJSON 加载建筑轮廓 Cesium.GeoJsonDataSource.load('./buildings.geojson', { clampToGround: true }).then(function (ds) { viewer.dataSources.add(ds); const entities = ds.entities.values; for (let i = 0; i < entities.length; i++) { const entity = entities[i]; // 给每个面加一个半透明填充,便于点击 entity.polygon.material = Cesium.Color.BLUE.withAlpha(0.3); entity.polygon.outline = true; entity.polygon.outlineColor = Cesium.Color.CYAN; // 绑定属性 entity.properties = { buildingId: 'B' + i, name: '模拟建筑' + i, height: 30 + Math.random() * 50 }; } }); // 点击拾取 const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); handler.setInputAction(function (click) { const picked = viewer.scene.pick(click.position); if (Cesium.defined(picked) && picked.id) { const props = picked.id.properties; if (props) { console.log('选中建筑:', props.name.getValue()); console.log('建筑高度:', props.height.getValue()); } } }, Cesium.ScreenSpaceEventType.LEFT_CLICK);这段代码的关键在于 clampToGround 要设 true,让矢量面贴合地形;填充透明度别太高,否则挡住倾斜摄影纹理。点击拾取时,Cesium 会优先返回最上层的 entity,所以矢量面要放在 tileset 之上。
另一个进阶用法是裁剪面。比如只看某一栋楼内部,可以用 ClippingPlaneCollection 把外部裁掉。
const clippingPlanes = new Cesium.ClippingPlaneCollection({ planes: [ new Cesium.ClippingPlane(new Cesium.Cartesian3(1.0, 0.0, 0.0), 0.0) ], edgeWidth: 1.0, edgeColor: Cesium.Color.WHITE }); tileset.clippingPlanes = clippingPlanes;这个平面方程的意思是保留 X 轴正方向一侧,负方向被裁掉。实际用的时候,平面法向量和距离要根据建筑位置算,通常配合鼠标交互动态更新。
最后说一个我自己的习惯:每次调完参数,先把 maximumScreenSpaceError 临时设成 64,让整个场景快速加载出来,确认位置和姿态没问题,再逐步调小到目标值。这样能避免在参数没对齐的情况下反复等加载,省下大量时间。希望帮到你。
本文还有配套的精品资源,点击获取