倾斜摄影与SHP转3D Tiles:从数据预处理到单体化实践
2026/9/14 1:58:28 网站建设 项目流程

简介:这是一款面向三维GIS与Web可视化开发者的格式转换工具,专门解决倾斜摄影模型、Shapefile矢量面数据及通用三维模型向3D Tiles体系转换的难题。工具支持Smart3D产出的OSGB倾斜摄影数据、带高度字段的Shapefile生成3D Tiles,也支持OSG/OBJ/FBX/3DS等通用模型转出glTF/glb,并能将b3dm还原为glb供调试使用。压缩包共179个文件,以dll运行库、gfs规则文件、csv坐标参数及少量exe主程序为主,整体约12.05MB,解压即用。资源还提供源码获取渠道,便于二次开发,已吸引3222人学习参考,适合需要批量转换三维数据、构建自定义3D Tiles场景的开发者直接套用。

1. 倾斜摄影与SHP转3D Tiles:两个方向一条管线

倾斜摄影建模讨不讨好,就看能不能被 Cesium 一类的 Web 引擎直接吃进去;原生的 OSGB 目录结构和 SHP 矢量文件,谁都没法在浏览器里直接调度。反直觉的是,看起来更"轻"的 SHP 转换往往比倾斜摄影更容易翻车:矢量数据要自己决定高度、自己处理坐标系、自己设计属性承载,而倾斜摄影的转换工具已经替你干完了大部分重活。下面把两条线拆开讲:倾斜摄影走 OSGB→b3dm 的切片管线,SHP 走 shp→GeoJSON→拉伸体→b3dm 的属性化管线,最后合并进同一场景。适合 GIS 开发、三维可视化工程师和数据生产人员照着搭,也适合正在评估转换工具的团队用来判断哪条路适合自己。

2. 倾斜摄影转3D Tiles:工具选择、LOD思路与三个必调参数

2.1 先看清 OSGB 目录里到底有什么

倾斜摄影成果不是孤立的一个模型文件,而是一棵按空间格子组织的瓦片树。磁盘上最常见的长相是这样的:

Data/ Tile_+000_+000/ Tile_+000_+000.xml Tile_+000_+000.osgb Tile_+001_+000/ Tile_+001_+000.xml Tile_+001_+000.osgb

每个 Tile 文件夹里的 XML 记录该瓦片的包围盒和子瓦片引用,OSGB 本体是带 LOD 的二进制场景图。转换工具做的第一件事就是遍历这棵树,把每个节点的三角网和纹理拆出来,再按 3D Tiles 的需求重建索引。这个过程的计算量不在建模本身,而在纹理压缩和顶点重排,所以转换耗时基本和三角面数量线性相关,也和纹理尺寸直接挂钩。

2.2 用现成工具跑通最小转换命令

倾斜摄影转 3D Tiles 的成熟工具链里,开源侧常见 osgb23dtiles,商业侧常见 CesiumLab 和 SuperMap iServer。它们转换思路一致:以 OSGB 的 Data 目录为输入,输出一块一块的 b3dm 瓦片和一个 tileset.json。本地机器的典型命令长这样:

osgb23dtiles -i /data/Terrain/Data -o /output/terrain_3dtiles \ --threads 8 --max-level 12 --min-level 3 \ --texture jpg --georeference epsg:4547

参数含义如下:-i指向含 XML 的 Data 目录,不是上一级工程;-o指定输出目录;--threads控制 CPU 并发,16 核机器建议 8~12;--max-level--min-level是保留的 LOD 层级区间;--texture jpg把 png 纹理重压成 jpg 以缩小体积;--georeference传入源数据坐标系,转换时把地方坐标换算到目标框架。各家工具参数名有差异,但语义基本就是这五个维度。

提示:第一次跑不要直接用全量数据,裁一个小区块先看输出;等 tileset.json 能正常加载,再放开范围,否则排错时间都花在加载等待上。

2.3 LOD、纹理和坐标三个必调点

LOD 层级决定了最高精度的抽稀程度。城市级大范围数据如果保留到 15 级以上,单瓦片三角形数虽少,但瓦片数量会爆炸,渲染调度压力大;8~12 级用于城市远景已经够用。实际取舍参考下表:

使用场景max-level单瓦片纹理说明
全市域底图10~12512×512首屏加载快,拉近略糊
重点片区详查15~182048×2048细节好,瓦片数量多
移动端10256×256兼顾流量和帧率

纹理会占掉大半输出体积。能把 80% 场景控制在 512 纹理,整个目录体积能比默认小一半以上;代价是近距离贴图模糊。克制的做法是近地细节单独保留一层高清,其余全部降采样。

坐标系是最隐蔽的坑。输出 tileset.json 后,打开根节点看 transform 的平移部分,如果数值是几十万、几百万那种大数,说明源数据是投影坐标;Cesium 默认按 WGS84 经纬度解释,模型会飞到海里。解决办法是转换时指定--georeference,或在输出后修改 tileset.json 中 transform 的平移值为当地经纬度弧度值。肉眼判断错位的标准是:加载后模型离地球表面几百公里,优先怀疑坐标系而不是数据范围;如果偏差只有几米,通常要检查投影带中央经线是否写对。

3. SHP转3D Tiles:从面到体的四步流水线

3.1 为什么 SHP 不能直接进 3D Tiles

SHP 是二维要素集,3D Tiles 里的 b3dm 是三维网格加属性表。二者的语义差:SHP 只有边界线和属性,没有任何"高度"和"厚度"的概念。所谓 SHP 转 3D Tiles,本质上是用软件替它补齐几何语义:给了高度,面变成柱体;给了符号,点变成标签;给了贴地,线变成地面 polyline。所以转换前必须先回答三个问题:哪个字段当高度,底座落在哪,属性传给谁。

3.2 处理 SHP 的最小 Python 流水线

在本地把 SHP 转成后续工具能认的 GeoJSON,是最灵活的一条路。下面这段代码覆盖读、转坐标、清洗、写出的完整过程:

import geopandas as gpd gdf = gpd.read_file("parcels.shp", encoding="utf-8") # 统一到 WGS84,避免输出文件里混着投影坐标 if gdf.crs and gdf.crs.to_epsg() != 4326: gdf = gdf.to_crs("EPSG:4326") # 清理空几何和无效记录,不然后面会生成破洞 gdf = gdf[~gdf.geometry.is_empty & gdf.geometry.is_valid] gdf.to_file("parcels.geojson", driver="GeoJSON")

重点在to_crs("EPSG:4326")is_valid过滤:很多 shp 多边形自相交,直接拉伸会在三维里产生飞面和重影;清洗后再转换能在源头消灭问题。属性字段在 GeoJSON 里原样保留,后面的 Batch Table 就靠它们。字段较多的原始 shp 建议先做一层取舍,把无关 CAD 属性丢掉,减少生成时 batch 表体积,这一步也能顺手把中文属性名替换成英文键名。

3.3 拉伸成体的几何运算

获得 GeoJSON 之后,把 Polygon 变成柱体的过程叫拉伸。高度来自属性字段,例如"楼层数×3.0+屋顶贴片厚度"。核心几何运算如下:

def extrude_polygon(coords, height=3.0): """把外环坐标拉伸成体,返回顶点和三角面索引""" verts = [[x, y, 0] for x, y in coords] + [ [x, y, height] for x, y in coords ] faces = [] n = len(coords) for i in range(n - 1): a, b = i, (i + 1) % (n - 1) faces.extend([ [a, a + n, b + n], [a, b + n, b] ]) return verts, faces

这段代码生成的是侧面,顶底面需要单独处理;实际使用中不要手写这一步,交给支持 GeoJSON 拉伸的转换器或 Cesium ion 的 shp 导入通道更稳。脚本适合做批量预处理。生成体之后检查法线朝向:拉伸体的面法线朝里在 Cesium 里看起来就是黑的,最常见的表现是白模渲染成黑模。

3.4 外边界提取、渔网分割和字段映射三个高频需求

"只保留外边界线"是转换高频需求,因为很多面带空洞和岛屿,拉伸后模型内部交叠。取外环的写法:

import geopandas as gpd gdf_outline = gpd.GeoDataFrame( geometry=gdf.geometry.exterior, crs=gdf.crs, data=gdf.drop(columns="geometry") ) gdf_outline.to_file("outline.geojson", driver="GeoJSON")

字段映射决定了属性能不能在 Cesium 里正常显示。GeoJSON 属性键名尽量用英文字段,中文放值里,否则某些引擎生成 Batch Table 时键名会乱码。具体映射关系可以整理成下表,转换前逐项确认:

原始字段示例映射目标用途
FID1021batchid唯一标识
CENG6height拉伸高度
USE住宅property弹窗属性
JZRQ2021property档案字段

遇到全市域、全省域范围时,推荐先用渔网分割:把大范围 SHP 按 1km×1km 或 5km×5km 格网切块,逐块转换,最后在 Cesium 里多个 tileset 并行加载。这样做的原因是,超大 GeoJSON 单次转 b3dm 容易内存溢出,切割后单文件加载时间从分钟级降到秒级,也方便后续按分区更新数据。

4. 倾斜模型和SHP白模合并:层级组织与单体化属性

4.1 为什么合并时要把两者分开加载

倾斜摄影和 SHP 白模的体量与调度方式差异太大。倾斜摄影一个城市可能有几万个瓦片,SHP 可能只有几百个要素;放在同一个 tileset.json 里,SHP 的瓦片会被按倾斜摄影的八叉树反复切分,浪费存储且加载时机难控制。常见做法是两条 tileset 并列,分别加载、分别控制显隐,在业务层拼成"一栋一栋可点选"的数字孪生底座。各自加载的代码只有几行:

const osgbTiles = await Cesium.Cesium3DTileset.fromUrl("/data/osgb/tileset.json"); const shpTiles = await Cesium.Cesium3DTileset.fromUrl("/data/shp/tileset.json"); viewer.scene.primitives.add(osgbTiles); viewer.scene.primitives.add(shpTiles);

分开加载还有个好处:可以单独设置style,例如让 SHP 白模半透明,露出倾斜模型的地表纹理;出问题时也能分别隐藏定位。

4.2 SHP 数据做 Cesium 3D Tiles 单体化的关键

单体化是 Cesium 3D Tiles 里被问得最多的场景。前提是 b3dm 里有 batch 表,把每个楼对应的三角面索引和一个属性 id 绑起来。SHP 转 3D Tiles 时,要素的唯一标识字段会被写入 batchid;点击拾取时,Cesium 通过 batchid 反查属性。最简单的拾取代码如下:

const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); handler.setInputAction(function (movement) { const picked = viewer.scene.pick(movement.position); if (Cesium.defined(picked)) { const ids = picked.id.getPropertyIds(); console.log(ids.length, ids); } }, Cesium.ScreenSpaceEventType.LEFT_CLICK);

如果这里拿不到属性,优先检查 batch 表生成环节;如果拿到但高亮面不完整,检查 glTF 的 batchid 是否覆盖全部三角面。这条链路就是"点选楼栋弹属性窗"的核心,数字孪生平台里几乎必改。

4.3 两种数据的 LOD 层级匹配

合并后要解决显示问题:倾斜模型拉近时细节丰富,SHP 白模拉近时会看到明显的直角和纹理缺失。让两者在视觉上统一,常见做法是 SHP 白模不做多级 LOD,直接一个层级,靠maximumScreenSpaceError控制切换;倾斜模型保留 8~12 级 LOD。这样远景时白模自动简化,近景时白模顶住细节,不会出现两套 LOD 互相打架的"闪烁"。参考设置如下:

数据类型LOD 策略maximumScreenSpaceError适用阶段
倾斜模型8~12 级8~10正式发布
SHP 白模单层16调试单体化

4.4 高度基准不一致时的处理

SHP 拉伸体的高度基准如果和倾斜摄影不一致,最常见的现象是楼体悬空或陷入地表。处理习惯按"倾斜模型先行,SHP 后置":先转完倾斜摄影,再从倾斜模型中采样地面高程,把采样结果回填到 SHP 属性字段,再执行拉伸。采样点按每栋楼 3~5 个点取平均,比单点可靠;数据量大时按网格聚合采样,避免在每栋楼上都算一遍。没有 DEM 时也可以从倾斜模型点云抽,但抽出的点要平滑,否则屋顶和地面会出现阶梯状破面。

5. 出图前的验证技巧:包围盒、属性拾取与遮挡缝隙

5.1 用调试开关快速定位飞包与错位

拿到转换结果后,先用空页面只加载 tileset,打开debugShowBoundingVolume做初步体检:

tileset.debugShowBoundingVolume = true; tileset.debugShowContentBoundingVolume = true;

绿色包围盒分布均匀,说明瓦片树层级正确;某块包围盒远离主体,说明该瓦片 transform 错误;包围盒互相穿插严重,说明 LOD 层级没切分好。调试完记得把这两个值设回 false,否则会产生额外 draw call,拉低帧率。

5.2 属性拾取与单体化的自测方法

自测单体化时,写一个拾取函数,把每次点击到的属性和 batchId 打印出来:

viewer.screenSpaceEventHandler.setInputAction(function (click) { const picked = viewer.scene.pick(click.position); if (Cesium.defined(picked)) { console.log(picked.id.batchId, picked.id.getPropertyIds()); } }, Cesium.ScreenSpaceEventType.LEFT_CLICK);

测试覆盖 5 个片区,每片点 10 栋,样本要包含边界、空洞和转角楼。个别栋点不出属性,大概率是 batchid 索引对不齐;全部点不出,问题在 Batch Table 本身或 glTF 里没写 batchid。用这个结论做排查能省掉大量来回沟通。

5.3 晃动缝隙与单体化高亮的一处优化

最后处理缝隙问题:SHP 白模与倾斜模型之间容易露缝,尤其在屋檐外挑处。处理技巧是给白模侧面做 0.2~0.5 米内缩,使白模边缘略微收进倾斜模型内部,视觉上不见缝,也不会因面片重叠产生明显闪面。调整量用属性表加一个inset字段控制,不写死在代码里。配合渔网分割的增量更新策略,每次只重建变化区域的白模瓦片,保留其他区域不动。全部调整完成后,在目标机器上做一次多分辨率访问测试,分别记录 2km、500m、50m 三个视距下的 FPS 和请求量,把 50m 视距下 FPS 低于 30 的瓦片区域重新生成后再收尾上线。

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

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

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

立即咨询