
简介3DTiles是Web端大规模三维地理空间数据可视化的常用技术这套测试数据面向Cesium开发者与三维GIS初学者包含“Test_1”“Test_2”两个中等场景可用于验证3DTiles的分块加载、b3dm格式解析、地理配准与性能调优等关键环节。压缩包共2000个文件以meta元数据文件、b3dm模型文件及少量json配置文件为主整体约883.58MBb3dm是带批次表的二进制glTF可批量管理模型属性并减少绘制调用meta与json则负责组织瓦片层级与场景结构便于观察典型工程目录。目前已有559人学习下载。通过实际操作这两个场景可直观理解不同分块策略对加载效率的影响掌握Cesium中相机交互、光照纹理及LOD切换的效果也能借助真实文件规模评估不同浏览器与设备上的渲染表现对于开发数字孪生、城市级CIM或WebGIS应用具有直接参考价值。1. 拆开这套 3DTiles 测试数据首先看到的是切片命名不是模型这套压缩包里没有“一个完整的 3D 模型”摆在根目录而是Tile_009_004_L23_000323000.b3dm、Tile_008_003_L18_0000t3.b3dm这类切片文件。第一次接触 Cesium 的人很容易被文件名唬住以为把 b3dm 拖进页面就能看到房子。实际上 Cesium 加载 3D Tiles 时入口永远是tileset.json它描述根节点、包围盒、几何误差、子节点引用关系b3dm 只是被引用的内容单元。两个中等场景Test_1和Test_2的价值正在这里它们不是成品展示而是用来验证 LOD 切换、b3dm 解析、BatchTable 属性读取这些真实运行时行为。适合做 Cesium 二次开发、倾斜模型转换、以及想弄明白 3D Tiles 内部组织方式的开发者拿来做实验床。2. 从 b3dm 文件名和二进制头部看 3DTiles 的数据组织2.1 文件名告诉你层级不告诉你坐标Tile_009_004_L23_000323000.b3dm这串字符里009、004更像格网索引L23表示四叉树层级最后 9 位数字我一般只当作 tile index不去解析它代表什么。因为同一套包里还会出现Tile_2_L22_003102000.b3dm这种少了009_004前缀的命名说明第三方导出工具并没有统一文件名规范。真正决定 tile 位置和范围的是tileset.json里的boundingVolume、transform和geometricError不是文件名。文件片段这包里的值常见含义使用建议Tile 前缀Tile_009_004四叉树/格网索引仅做肉眼区分L 层级L23/L22/L18细节层级层级越高模型越精细尾部索引000323000tile 内的位置编码不要当坐标用扩展名.b3dm带 BatchTable 的二进制 glTF加载入口在 tileset.json2.2 b3dm 的二进制布局没你想的复杂b3dm 全称是 Binary glTF with Batch Table本质上是一个 28 字节头部后面跟着 FeatureTable JSON、FeatureTable 二进制数据、BatchTable JSON、BatchTable 二进制数据最后再嵌一段 GLB。FeatureTable 描述整个 tile 的全局信息比如RTC_CENTER、BATCH_LENGTHBatchTable 描述每个 feature 的属性比如建筑 id、楼层、权属。Cesium 做单体化点击时读的就是 BatchTable。偏移字节长度字段作用04magic固定为b3dm44version通常为 184byteLength整个 b3dm 文件长度124featureTableJSONByteLengthFeatureTable JSON 长度164featureTableBinaryByteLengthFeatureTable 二进制长度204batchTableJSONByteLengthBatchTable JSON 长度244batchTableBinaryByteLengthBatchTable 二进制长度写一个最小解析脚本不依赖任何 3D 库import fs from node:fs; const buf fs.readFileSync(process.argv[2]); const header { magic: buf.toString(utf8, 0, 4), version: buf.readUInt32LE(4), byteLength: buf.readUInt32LE(8), featureTableJsonByteLength: buf.readUInt32LE(12), featureTableBinaryByteLength: buf.readUInt32LE(16), batchTableJsonByteLength: buf.readUInt32LE(20), batchTableBinaryByteLength: buf.readUInt32LE(24) }; console.log(header:, header); // FeatureTable JSON 从偏移 28 开始 const ftJsonStart 28; const ftJson buf.toString(utf8, ftJsonStart, ftJsonStart header.featureTableJsonByteLength); console.log(featureTableJson:, ftJson); // glTF 内容在 FeatureTable 和 BatchTable 全部结束之后 const glbStart ftJsonStart header.featureTableJsonByteLength header.featureTableBinaryByteLength header.batchTableJsonByteLength header.batchTableBinaryByteLength; console.log(embedded magic at glbStart:, buf.toString(utf8, glbStart, glbStart 4));这个脚本用readUInt32LE读长度因为 b3dm 头部是小端序。如果你用大端读magic 之后的 version 和 byteLength 全会错位。跑完后如果看到featureTableJson里有RTC_CENTER说明该 tile 的局部坐标依赖这个中心点做整体偏移如果BATCH_LENGTH大于 0说明这个 b3dm 里有可被前端点击的 feature。2.3 tileset.json 才是把切片串成树的骨架一个最简tileset.json长这样{ asset: { version: 1.0 }, geometricError: 500, root: { boundingVolume: { box: [ -2700000, 4500000, 3800000, 300, 0, 0, 0, 150, 0, 0, 0, 80 ] }, geometricError: 200, refine: ADD, content: { uri: Tile_009_004_L23_000323000.b3dm }, children: [] } }geometricError决定当前视角下是否需要细化数值越小模型越精细。refine: ADD表示父节点和子节点同时显示REPLACE表示子节点替换父节点。城市级场景一般用 REPLACE避免父层大模型和子层细节模型叠加造成双层重叠。提示Cesium 加载的是这棵 tile 树不是单个 b3dm。把一个 b3dm 直接丢给Cesium3DTileset.fromUrl是加载不出来的。3. 把 Test_1 / Test_2 跑起来加载参数与 LOD 切换3.1 先起一个本地 HTTP 服务3D Tiles 的加载依赖浏览器 fetch直接双击tileset.json走file://会被 CORS 拦住页面控制台一片红。最省事的办法是在解压目录里起 HTTP 服务cd /path/to/解压后/Test_1 python3 -m http.server 8080浏览器访问http://localhost:8080/tileset.json能看到 JSON 内容就说明服务起来了。端口可以换只要 Cesium 页面里的 URL 和这个端口一致。Test_2同理没必要两个服务同时开加载时把 URL 换成/Test_2/tileset.json就行。3.2 Cesium 侧加载最小实现用一个空白 HTML 页面加载Test_1!doctype html html head meta charsetutf-8 title3DTiles Test_1/title style html, body, #container { width: 100%; height: 100%; margin: 0; overflow: hidden; } /style script srchttps://cesium.com/downloads/cesiumjs/releases/1.119/Build/Cesium/Cesium.js/script link hrefhttps://cesium.com/downloads/cesiumjs/releases/1.119/Build/Cesium/Widgets/widgets.css relstylesheet /head body div idcontainer/div script const viewer new Cesium.Viewer(container, { baseLayer: false, scene3DOnly: true, animation: false, timeline: false }); async function loadScene(url) { const tileset await Cesium.Cesium3DTileset.fromUrl(url, { maximumScreenSpaceError: 16, skipLevelOfDetail: true, preferLeaves: true, dynamicScreenSpaceError: true, cullWithChildrenBounds: true }); viewer.scene.primitives.add(tileset); await viewer.flyTo(tileset); return tileset; } loadScene(http://localhost:8080/tileset.json); /script /body /html这段代码里baseLayer: false是为了不让 Cesium 默认去加载在线影像省掉 Ion token 的干扰Cesium3DTileset.fromUrl是新式异步加载接口返回 Promise所以用await。关键参数在下面的表里参数默认值建议值说明maximumScreenSpaceError168-32越小越精细越吃渲染性能skipLevelOfDetailfalsetrue跳过中间层级快速出图preferLeavesfalsetrue优先加载叶子节点细节更早出现dynamicScreenSpaceErrorfalsetrue城市级大场景下更符合视角中心优先策略cullWithChildrenBoundsfalsetrue用子节点包围盒提前剔除看不见的 tilemaximumScreenSpaceError是最常调的参数。做性能测试时从 16 增加到 32能明显看到显卡压力下降但建筑边缘会出现跳变切回 8 会看得更细但加载量成倍上升。3.3 用 tileLoad 事件观察分块切换加载逻辑跑通后可以挂事件看每个 tile 的实际加载情况const tileset await loadScene(http://localhost:8080/tileset.json); tileset.tileLoad.addEventListener((tile) { console.log( loaded tile:, tile.content.url, features:, tile.content.featuresLength, geometricError:, tile.geometricError ); }); tileset.tileFailed.addEventListener((error) { console.warn(failed:, error.url, error.message); });tile.content.featuresLength是当前 b3dm 里 feature 的数量。如果全是 0说明这个 b3dm 里没有 BatchTable 属性后续做单体化点击时拿不到建筑信息。geometricError会让 LOD 切换的边界非常直观当视角拉远加载的是 L18 这种大粗模拉近控制台刷出来的就变成 L23 细模。4. 从测试场景到生产数据shp 转 3dtiles、fbx 转 3dtiles 和单体化4.1 先认清一条完整链路Cesium 本身不读.fbx也不直接消费.shp。所有非 glTF 格式必须走到glTF/GLB再包装成b3dm最后用tileset.json组织成树。这条链路里最容易走的弯路是拿到一套 fbx 就想让 Cesium 直接加载结果发现加载器根本不认识这个格式。环节输入输出常用工具坐标/属性清洗shpGeoJSON 或带属性 glTFogr2ogr、QGIS模型导出fbxglbBlender、Assimp格式打包glbb3dm3d-tiles-tools切片组织b3dm 集合tileset.json3d-tiles-tools、Cesium ion、CesiumLab4.2 fbx 转 3dtiles别绕过 glTF我处理 fbx 转 3dtiles 的常规动作是先统一单位导出成 glb再包 b3dm# 先用 Blender 或 Assimp 把 fbx 导出为 glb单位必须是米 npx 3d-tiles-tools convert -i building.glb -o building.b3dm --type b3dm # 如果 models 目录下已经有一批 b3dm生成一个最小 tileset.json npx 3d-tiles-tools createTileset -i models -o models/tileset.json--type b3dm告诉转换器输出格式老版本工具可能用-t b3dm不确定时先跑npx 3d-tiles-tools --help。转换后不要立刻上传先用第 2 章那个脚本跑一下embedded magic能打印出glTF才算封装成功。FBX 的坐标系经常是 Z-up而 glTF 是 Y-up导出 glb 时如果没做轴转换进 Cesium 后楼体会横躺。4.3 shp 转 3dtiles先把二维变三维shp 转 3dtiles 不是一个按钮能解决的事。shp 的 geometry 是二维面必须先有高度或楼层字段才能挤出体块。我的做法是先导出属性再交给建模环节ogr2ogr -f GeoJSON -select bld_id,height,layer -lco RFC7946YES buildings.json buildings.shp-select只保留bld_id、height、layer这几个后续要写进 BatchTable 的字段避免属性表里一堆无关字段都塞进 b3dm。RFC7946YES是让输出 GeoJSON 使用标准坐标顺序。拿到的 GeoJSON 里如果height是空值要先估算楼层高度否则转出来的 3D Tiles 只是一片贴地的面不会有体积感。4.4 cesium 3dtiles 单体化batch table 才是关键单体化的前提是 b3dm 的 BatchTable 里有每个 feature 的属性。前端点击时拿属性用这段代码const handler new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); handler.setInputAction((movement) { const picked viewer.scene.pick(movement.endPosition); const feature picked picked.feature; if (!feature || !(feature instanceof Cesium.Cesium3DTileFeature)) return; const props {}; const names feature.getPropertyNames(); for (let i 0; i names.length; i) { props[names[i]] feature.getProperty(names[i]); } console.table(props); feature.color Cesium.Color.fromCssColorString(#ffcc00); }, Cesium.ScreenSpaceEventType.LEFT_CLICK);scene.pick返回的对象里带feature这个Cesium3DTileFeature就是渲染时从 BatchTable 带出来的一个独立建筑。getPropertyNames()能拿到所有属性名再逐个getProperty(name)就能读取 id、楼层、用途。点击高亮只改feature.color做演示足够正式项目里要维护一套选中 id 列表统一给一组 feature 上色否则切视角后 LOD 重新调度颜色会丢。5. 低成本验证一套 3DTiles 测试数据有没有切坏拿到Test_1/Test_2后与其在浏览器里肉眼找 bug不如先跑一遍官方校验器npx 3d-tiles-validator -i Test_1/tileset.json --reportFile validation/report.json这条命令会检查tileset.json的资产版本、根节点包围盒、父子包围盒是否互相包含、每个 b3dm 是否能被正常解析。如果报告里大面积报错说明这套数据不是标准切片而是手工拼的 b3dm 列表先修分块再调渲染。跑完后重点看几个高频问题现象优先检查原因模型整体偏移到海里transform和boundingVolumeEPSG:4978 地心坐标没有对齐建筑横躺或翻转glb 导出的轴方向glTF 是 Y-upFBX 常见 Z-up远处白模拉近才细maximumScreenSpaceError太大SSE 阈值太高细层被裁掉点击没有任何属性BatchTable 没写入转换时没把 shp 属性表带进 b3dm控制台大量 404content.uri路径错误tileset.json 里的相对路径与文件实际位置不一致最后再执行一次npx 3d-tiles-validator -i Test_2/tileset.json --reportFile validation/report2.json把两份报告里的geometricError和 tile 数量对比一下就能看出Test_1和Test_2的分块策略差异到底出在根节点切分粒度还是每个 b3dm 的内容颗粒度上。本文还有配套的精品资源点击获取