OSGB与SHP转3D Tiles:倾斜摄影、矢量数据与Cesium单体化实战
2026/9/14 1:50:02 网站建设 项目流程

简介:一款面向三维GIS与Web可视化开发者的3DTiles格式转换工具,覆盖倾斜摄影、Shapefile及通用模型转glTF等常见生产需求。工具重点支持Smart3D的OSGB组织方式的倾斜数据转换,也可读取带高度字段的Shapefile面数据生成3DTiles,同时提供OSG/OBJ/FBX/3DS等通用模型转glTF/glB能力,以及B3DM转GLB功能,便于调试和测试。压缩包共179个文件,以dll动态库、csv坐标与参数表、wkt与xsd辅助定义文件为主,另含可执行程序及配置文档,整体约12MB,轻量便携。资源提供源码,开发者可结合博客说明进行二次改造或嵌入自有流程。目前已有3222人学习使用,适合需要批量处理倾斜摄影、矢量边界或模型数据的三维开发人员与GIS工程师。

1. 倾斜摄影、SHP 与 3D Tiles:为什么格式转换工具决定了项目上限

项目竣工验收时往往同时拿到两套数据:一套是倾斜摄影建模软件输出的 OSGB 格式真三维模型,另一套是业务部门导出的 SHP 格式建筑边界与权属面。前一套数据量大、观感真实,却点不到具体建筑;后一套属性完整、边界清晰,但没有高度也没有纹理。把两者统一转换成 3D Tiles 格式,是 Cesium 场景里做展示与业务查询的前提。这一步不是另存为:坐标系要重投影,纹理要压缩,LOD 要重排,SHP 的属性还得写进瓦片批量表。下面按“格式底子 → 倾斜摄影转换 → SHP 转换 → 单体化 → 验证排错”的顺序,把这条链路讲透。

2. 读格式底子:OSGB 瓦片、SHP 属性表与 3D Tiles 结构

转换工具再怎么封装,底层都在做同一件事:把源数据的几何、纹理、属性重新组织成目标格式。理解 OSGB、SHP、3D Tiles 三者的组织方式,后面调参数和排错才有依据。

2.1 OSGB:倾斜摄影输出的 LOD 目录与 metadata.xml

倾斜摄影建模软件跑完空三和纹理映射后,输出的 OSGB 数据是一个按金字塔层级组织的瓦片目录。以 ContextCapture 为例,典型结构是Data/Tile_+000_+000/下按 L0、L1、L2 分目录,每个目录里是多个.osgb文件和一份metadata.xml。metadata.xml 记录工程坐标系、原点、范围和各层级关系,转换工具的第一步基本都是从读它开始。

这里有个容易忽视的细节:OSGB 瓦片内部顶点坐标是相对所在分块的局部坐标,数据类型是 float32。当工程原点离某个分块较远,局部坐标数值变大,float32 精度不够,转换后会出现“远瓦片抖动”或顶面共面。所以成熟转换工具会做两步处理:先把局部坐标还原到工程坐标,再统一输出到目标坐标系;大范围工程还会按区块重设局部原点。理解了这一层,第 3 章里坐标偏移参数的用途就清楚了。

2.2 SHP 的三角结构:shp/shx/dbf 缺一不可

SHP 严格说不是一个文件,而是一组文件的集合。.shp存几何,.shx是几何索引,.dbf是属性表,三者靠同名前缀关联。转换到 3D Tiles 时,几何要变成三角网或要素外包围体,属性要写进 batchTable 供前端查询。最常见的拦路问题是.dbf字段类型不统一,比如“楼层数”有的记录是 0,有的是“—”,ogr2ogr 读出来会直接报字段类型警告。

  1. 先用 QGIS 或 ogrinfo 检查字段类型
  2. 把数字字段统一为整型或浮点,空值填 0
  3. 再执行后续转换

另外 SHP 有 2GB 体积限制,大型城区数据在转换前就要分块,这也是 4.4 节里渔网分割分批处理存在的原因。

2.3 3D Tiles 容器:tileset.json 与 b3dm/pnts/i3dm 的适用场景

3D Tiles 由两部分组成:一个描述瓦片树的 tileset.json,以及若干瓦片内容文件。每个瓦片节点都声明了包围体和几何误差,Cesium 按相机距离计算屏幕空间误差,决定加载到哪一层。下面是一份最小 tileset.json,能直观看出节点骨架:

{ "asset": { "version": "1.0" }, "geometricError": 500, "root": { "boundingVolume": { "region": [ 2.026682, 0.680678, 2.045482, 0.689478, 0, 500 ] }, "geometricError": 50, "refine": "ADD", "content": { "uri": "tiles/0.b3dm" }, "children": [ { "boundingVolume": { "box": [0, 0, 0, 120, 0, 0, 0, 80, 0, 0, 0, 60] }, "geometricError": 0, "content": { "uri": "tiles/1.b3dm" } } ] } }

region 数组前四个数是西、南、东、北边界,单位是弧度而不是角度,很多手写 tileset 的报错都出在这里;后两个数是最小、最大高度。box 数组则是中心坐标加三个半轴向量。geometricError 控制 LOD 切换阈值,数值越大越容易被替换成子级;refine 的ADD表示父子同显,REPLACE表示父级替换为子级,倾斜摄影转换默认多用REPLACE

2.3.1 三种瓦片内容类型的适用场景

瓦片内容文件按数据类型分三类:

瓦片内容适用数据典型来源
b3dm批量三角网模型,带纹理与属性OSGB 倾斜摄影、手工建模、SHP 拉伸体
pnts点云激光雷达、倾斜摄影点云
i3dm大量相同模型实例路灯、树木、井盖

倾斜摄影转出的瓦片绝大多数是 b3dm;SHP 转出来的既有 b3dm,也有把点要素直接转成 pnts 的情况。

2.3.2 b3dm 内部的要素表与批次表

b3dm 文件内部是 glTF 二进制体加要素表(featureTable)和批次表(batchTable)。要素表记录BATCH_LENGTH,即该瓦片里的要素个数;批次表存每个要素的属性键值。这个结构决定了前端能不能按要素拾取,第 5 章的单体化也依赖它。

3. 倾斜摄影 OSGB 转 3D Tiles:主流程、命令行与三个必调参数

拿到 OSGB 数据,先看 metadata.xml 里的坐标系和原点,再选工具。转换结果好不好,最终取决于坐标系、LOD 层级、纹理压缩三个参数。

3.1 倾斜摄影转换的两种工具路线

国内 WebGIS 项目里最省事的路线是直接用 CesiumLab 的倾斜模型转换模块。它把坐标转换、LOD 重采样、纹理压缩、瓦片重分块封装成一次操作,界面上核心选项是输出坐标系、LOD 最大层级、纹理压缩格式和顶点压缩开关。一个 80GB 的 OSGB 工程,普通工作站跑完大约几小时,产物体积通常能压到原始数据的三分之一到二分之一。

如果要自动化批处理,就上命令行路线。社区里有不少把 OSGB 转 b3dm 的开源转换器,调用形态大致如下:

# 以通用参数形态调用开源 OSGB 转 3D Tiles 工具 osgb23dtiles \ --input /data/osgb \ --output /data/3dtiles \ --crs EPSG:4547 \ --max-level 18 \ --threads 8

说明:--crs指的是源数据坐标系,EPSG:4547 是 CGCS2000 / 3-degree Gauss-Kruger zone 41 的常见编号;--max-level控制最精细层,设得过大只会增加瓦片数和请求压力;--threads控制并行度,一般不超过物理核心数。不同发布版本的参数名略有出入,跑之前先执行--help确认,别拿网上的命令直接套。

3.2 转换时的四个关键参数:坐标系、LOD、纹理、顶点精度

3.2.1 坐标偏移与浮点精度:漂移的根源

如果源工程是高斯-克吕格投影,比如 EPSG:4547,坐标值可能到 4×10⁷ 米量级。3D Tiles 的瓦片坐标最终要落到以地球质心为原点的 ECEF 坐标系,中间计算一旦用 float32 存这个大数,精度就只有米级,模型会整体漂移或扭曲。所以转换工具必须“先减原点偏移,再转入 ECEF”:把大坐标换算成相对工程原点的局部坐标,再在 tileset 根节点用 transform 把局部坐标系挂到全球坐标上。这就是倾斜摄影转换界面里总有一项“坐标偏移”的原因。

我一般把源坐标系选 EPSG:4547,输出坐标系直接选 WGS84 或 CGCS2000,让工具自动算偏移和旋转矩阵;如果源数据是工地自设的任意坐标系,就手动填原点经纬度和高程,否则 99% 会偏移。

3.2.2 纹理压缩与纹理尺寸上限

OSGB 瓦片里的纹理大多是 jpg,单张 512×512 到 2048×2048。转换时把纹理转成 WebP,质量设 75 到 85,通常能再降一半体积;远景 LOD 层甚至可以降到 60。下表是我常用的一组设置:

参数推荐值说明
纹理格式WebP对倾斜摄影影像压缩率明显优于 jpg
WebP 质量高层 85 / 低层 60远景瓦片肉眼几乎无差异
纹理尺寸上限1024×1024超过部分缩放,避免单瓦片过大
LOD 最大层级源数据最大层级或小 1 级保留全部层级但检查叶子瓦片数量
顶点坐标压缩开启顶点误差约毫米级,影响可接受

3.3 检查转换产物:文件数量与 tile 完整性

转换完先别急着进 Cesium,检查产物结构:

find /data/3dtiles -name "*.b3dm" | wc -l du -sh /data/3dtiles

瓦片数只有几十个,多半是 LOD 重采样丢层,或建模时只建到某一层;把--max-level降到源数据实际层级再跑一次。单个 b3dm 体积异常小(1MB 以下)时,检查对应区域有没有孔洞,这通常是空三稀疏区或建筑边缘悬空引起的,转换工具只能保证不崩,不能修复数据空洞。

4. SHP 转 3D Tiles:矢量拉伸、属性映射与渔网分批

SHP 转 3D Tiles 不像 OSGB 那么重,不涉及 LOD 重采样,核心是把面、线、点变成三维体或点云,并保证属性进 batchTable。多数项目里 SHP 是建筑轮廓线,目标产物是带高度的 b3dm。

4.1 第一步:ogr2ogr 把 SHP 转 GeoJSON 并做属性清洗

转之前,我习惯先用 GDAL 的 ogr2ogr 把 SHP 转成 GeoJSON,做一次数据体检。GeoJSON 结构简单,前端也能直接用,后续进转换工具或脚本都方便:

ogr2ogr -f GeoJSON -t_srs EPSG:4326 building.geojson building.shp

这条命令做两件事:把几何重投影成 WGS84 经纬度,同时把.dbf属性表复制成 GeoJSON 的 properties 字段。-t_srs指定目标坐标系,位置写错是最常见的低级错误。如果 SHP 缺.prj文件,GDAL 会默认按 EPSG:4326 读,结果是位置错乱。转换前先执行:

ogrinfo -al -so building.shp

注意看输出里的要素数量和坐标范围。出现ERROR: Unable to open datasource说明 shp 读取有问题,先确认 shp/shx/dbf 三个文件是否齐备,再决定是否用修复工具重建。

4.1.1 用 SQL 过滤与字段重命名

ogr2ogr 支持 SQL 裁剪属性,对只转部分要素的场景很实用:

ogr2ogr -f GeoJSON -t_srs EPSG:4326 \ -sql "SELECT id, name, floor AS floor_num, height FROM building WHERE floor > 0" \ building_sub.geojson building.shp

字段名尽量用英文和数字组合,不要带中文或空格,部分解析器对这类字段名兼容性差。这里用 SQL 别名把 floor 改成 floor_num,顺手把无意义的 0 层建筑过滤掉。

4.2 第二步:按高度字段拉伸多边形并导出 GLB

有了干净的 GeoJSON,下一步把每个面按属性高度拉伸成体。这一步可以在 CesiumLab 的 SHP 转换模块里做,也可以用脚本。Python 最小实现如下:

import geopandas as gpd import trimesh from trimesh.creation import extrude_polygon gdf = gpd.read_file("building.geojson") def feature_to_mesh(geometry, height): if geometry.geom_type == "MultiPolygon": parts = [feature_to_mesh(p, height) for p in geometry.geoms] return trimesh.util.concatenate(parts) return extrude_polygon(geometry, height=height) for idx, row in gdf.iterrows(): # height 字段缺失或空值时兜底 10 米 h = float(row.get("height", 10) or 10) mesh = feature_to_mesh(row.geometry, h) mesh.export(f"glb/{idx}.glb")

extrude_polygon把二维面沿 Z 轴拉伸成三棱柱型实体,高度直接取 dbf 里的 height 字段。这段代码没有处理多边形内部孔洞(带天井的建筑会出问题),实际项目里可以用 shapely 的interiors做减法,或者在 CesiumLab 里完成这一步,脚本只做属性预检。注意or 10兜底很必要,个别要素缺字段不至于让整个循环崩溃。

4.3 第三步:属性进 batchTable,前端按要素拾取

生成的 GLB 还需要聚合成 b3dm,并把 id、name、height 写进 batchTable。b3dm 的要素表记录BATCH_LENGTH,batchTable 里每个字段对应一个属性列。转换后的前端拾取代码如下:

const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); handler.setInputAction((click) => { const picked = viewer.scene.pick(click.position); if (picked && picked.primitive && picked.primitive instanceof Cesium.Cesium3DTileset) { const feature = picked.primitive.getFeature(picked.batchId); if (feature) { console.log('属性:', feature.getProperty('name'), feature.getProperty('height')); } } }, Cesium.ScreenSpaceEventType.LEFT_CLICK);

scene.pick拿到点中的对象,b3dm 瓦片会带上 batchId,通过getFeature拿到批次表里的属性对象,然后用getProperty查询字段值。转换时没写属性,getProperty会返回 undefined,所以这一步也是属性映射的验证手段。

4.4 大面积 SHP 的渔网分割分批转换

全市范围的建筑轮廓 SHP,直接拉伸转 b3dm 很容易出现超大瓦片,单个 b3dm 几十 MB,浏览器要么超时要么卡死。常见做法是渔网分割 SHP:按经纬度格网把大面切到多个网格内,逐块转换,边界处用缓冲区重叠:

# 按矩形窗口切分 GeoJSON,窗口大小视要素密度调整 ogr2ogr -f GeoJSON -clipsrc 116.30 39.80 116.40 39.90 grid_a.geojson building.geojson

分块尺寸按目标瓦片体量反推:单块转换后 b3dm 总量控制在 5MB 以内比较稳妥。先分大块试跑,再按结果微调边界。注意要素与格网边界相交的问题,通常对边界要素做双写,避免两个网格之间出现空洞。

5. SHP 与倾斜模型结合:动态单体化与切割单体化

OSGB 转成 3D Tiles 之后,倾斜模型是一整片连续三角网,没有建筑边界。要做出“点击每个建筑查属性”的效果,需要靠 SHP 给模型划分语义边界,这就是单体化。

5.1 单体化的两种实现路径对比

单体化有两条路线。一条是切割单体化:用 SHP 多边形把倾斜模型瓦片切分开,每个建筑成为独立瓦片或独立 batch,交互精度高,但切缝处理复杂、转换时间长。另一条是动态单体化:模型不切,只把 SHP 作为拾取边界叠加,点击时映射属性,成本低、交付快。

方案实现成本交互精度适用场景
切割单体化高,需重切瓦片并处理接缝精确到建筑面对每个建筑有编辑、高亮需求
动态单体化低,SHP 转 ID 映射即可面内可点,边缘易漏快速交付的属性查询项目
5.1.1 动态单体化的生产流程与属性字段设计

动态单体化的生产流程通常是:先给 SHP 每个多边形分配一个全局唯一 ID,转成一张 ID 与属性的映射表;再把 SHP 与倾斜模型叠合,发现某个瓦片与哪些多边形相交,就把该瓦片里的三角形按多边形分成多个 batch,并将 ID 写入 b3dm 的 batchTable。前端点击某个瓦片时,通过 batchId 反查 ID,再去映射表里拿业务属性。字段命名建议统一用fidfeature_id,工具配置里保持一致。

5.2 动态单体化:把 SHP 属性挂到倾斜模型上拾取

在 Cesium 场景里,加载倾斜模型 tileset 之后,如果再叠加一份 SHP 转出的 GeoJSON,也可以实现轻量动态单体化。点击事件先判断是否命中 GeoJSON 面,再高亮并读取属性:

const tileset = await Cesium.Cesium3DTileset.fromUrl('/data/3dtiles/tileset.json'); viewer.scene.primitives.add(tileset); const ds = await Cesium.GeoJsonDataSource.load('/data/building.geojson', { fill: Cesium.Color.YELLOW.withAlpha(0.0), stroke: Cesium.Color.CYAN, }); viewer.dataSources.add(ds); const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); let lastPicked = null; handler.setInputAction((click) => { const picked = viewer.scene.pick(click.position); if (picked && picked.id) { const feature = picked.id; if (lastPicked) { lastPicked.polygon.material = Cesium.Color.YELLOW.withAlpha(0.0); } feature.polygon.material = Cesium.Color.CYAN.withAlpha(0.4); lastPicked = feature; console.log(feature.properties.getValue(viewer.clock.currentTime)); } }, Cesium.ScreenSpaceEventType.LEFT_CLICK);

GeoJSON 加载后每个 feature 对应一个 Entity,scene.pick返回的picked.id就是它;properties.getValue()读出转 GeoJSON 时保留的 dbf 属性。用lastPicked复位上一次高亮,避免多选叠加。这种方式的缺点是模型边缘漏选,因为 SHP 边界和模型网格边缘存在偏差。

提示:动态单体化要求 SHP 与倾斜模型严格配准。SHP 坐标系和倾斜模型输出坐标系不一致时,先用 ogr2ogr 统一到同一 EPSG 再加载,否则视觉上始终差一截。

6. 转换后的验证与三处高频故障点

6.1 用 Cesium 页面验证 3D Tiles 加载状态

转换完先在本地起静态服务器,写个最小页面验证:

<!DOCTYPE html> <html lang="zh-CN"> <body> <div id="cesiumContainer"></div> <script src="/libs/cesium/Cesium.js"></script> <script> const viewer = new Cesium.Viewer('cesiumContainer', { infoBox: false }); const tileset = new Cesium.Cesium3DTileset({ url: '/data/3dtiles/tileset.json', }); viewer.scene.primitives.add(tileset); viewer.zoomTo(tileset); </script> </body> </html>

Chrome 打开后看 Network 面板:视角移动时新 b3dm 不断被请求,控制台没有Failed to load tile报错,就算通过。若报错,检查该瓦片的 uri 路径是否相对路径,以及跨域头是否放行。

6.2 三个高频故障点:坐标偏移、纹理发黑、属性点不到

排障按“先位置,再显示,后点选”的顺序走。位置偏移一般源于坐标系或原点设置错,回到 OSGB metadata.xml 核对源坐标,回 SHP 的.prj核对目标坐标。纹理发黑是瓦片 glTF 的 UV 或法线有问题,尤其 OSGB 转换后某些工具输出法线反向或纹理坐标越界,常见处理是换顶点压缩方式或换转换器重跑。

属性点不到分两种情况:一是 dbf 没写进 batchTable,b3dm 里根本没有属性;二是 SHP 转单体化时 ID 映射错位,批次表顺序和几何顺序不一致。验证方法是先打印feature.getPropertyNames(),数组为空就是属性没进去;数组有值但取出来不对,再去查转换工具的字段映射关系而不是怀疑前端。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询