
简介一款面向三维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 读出来会直接报字段类型警告。先用 QGIS 或 ogrinfo 检查字段类型把数字字段统一为整型或浮点空值填 0再执行后续转换另外 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大量相同模型实例路灯、树木、井盖倾斜摄影转出的瓦片绝大多数是 b3dmSHP 转出来的既有 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对倾斜摄影影像压缩率明显优于 jpgWebP 质量高层 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, heightheight) 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(fglb/{idx}.glb)extrude_polygon把二维面沿 Z 轴拉伸成三棱柱型实体高度直接取 dbf 里的 height 字段。这段代码没有处理多边形内部孔洞带天井的建筑会出问题实际项目里可以用 shapely 的interiors做减法或者在 CesiumLab 里完成这一步脚本只做属性预检。注意or 10兜底很必要个别要素缺字段不至于让整个循环崩溃。4.3 第三步属性进 batchTable前端按要素拾取生成的 GLB 还需要聚合成 b3dm并把 id、name、height 写进 batchTable。b3dm 的要素表记录BATCH_LENGTHbatchTable 里每个字段对应一个属性列。转换后的前端拾取代码如下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再去映射表里拿业务属性。字段命名建议统一用fid或feature_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 对应一个 Entityscene.pick返回的picked.id就是它properties.getValue()读出转 GeoJSON 时保留的 dbf 属性。用lastPicked复位上一次高亮避免多选叠加。这种方式的缺点是模型边缘漏选因为 SHP 边界和模型网格边缘存在偏差。提示动态单体化要求 SHP 与倾斜模型严格配准。SHP 坐标系和倾斜模型输出坐标系不一致时先用 ogr2ogr 统一到同一 EPSG 再加载否则视觉上始终差一截。6. 转换后的验证与三处高频故障点6.1 用 Cesium 页面验证 3D Tiles 加载状态转换完先在本地起静态服务器写个最小页面验证!DOCTYPE html html langzh-CN body div idcesiumContainer/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 /htmlChrome 打开后看 Network 面板视角移动时新 b3dm 不断被请求控制台没有Failed to load tile报错就算通过。若报错检查该瓦片的 uri 路径是否相对路径以及跨域头是否放行。6.2 三个高频故障点坐标偏移、纹理发黑、属性点不到排障按“先位置再显示后点选”的顺序走。位置偏移一般源于坐标系或原点设置错回到 OSGB metadata.xml 核对源坐标回 SHP 的.prj核对目标坐标。纹理发黑是瓦片 glTF 的 UV 或法线有问题尤其 OSGB 转换后某些工具输出法线反向或纹理坐标越界常见处理是换顶点压缩方式或换转换器重跑。属性点不到分两种情况一是 dbf 没写进 batchTableb3dm 里根本没有属性二是 SHP 转单体化时 ID 映射错位批次表顺序和几何顺序不一致。验证方法是先打印feature.getPropertyNames()数组为空就是属性没进去数组有值但取出来不对再去查转换工具的字段映射关系而不是怀疑前端。本文还有配套的精品资源点击获取