)
先说个场景。项目里拿到一批城市级三维瓦片后缀是.b3dm在 Cesium 这类三维 GIS 平台上加载非常顺滑点击建筑、弹窗、属性查询都能做。可模型一旦要拿进 Blender 4.3 里修一修、换换材质或者把其中某栋楼单独拎出来交给下游系统展示问题就来了Blender 根本不认 b3dm拖进去直接没反应。网上搜一圈要么让你装商业 GIS 软件要么让你用在线转换服务文件大得离谱传上去还要等半天。这篇文章就是解决这个问题的。我会把 b3dm 这层“壳子”拆开先讲清楚它内部到底存了什么然后给一个不依赖第三方库的 Python 提取脚本把内层模型一条条扒出来导入 Blender 4.3 后按 batchId 或材质做“单体化”也就是把一个连成片的三角网拆成一个个独立建筑最后统一导成 FBX 交付。适合谁看做三维 GIS 数据处理、数字孪生项目、智慧城市模型治理的朋友以及手里只有 b3dm 交付成果、想进 Blender 做二次编辑的建模师。下面直接开干。1. 先搞清楚 b3dm 到底是什么——它离 FBX 其实只差一步1.1 b3dm 本质上是个“容器”不是单纯模型格式很多人以为 b3dm 是一种模型格式实际上它是 3D Tiles 瓦片体系中的一种容器格式。一个后缀为.b3dm的文件内部最核心的数据是一段标准的 glb也就是二进制 glTF。glb 是什么就是 glTF 格式的二进制版本专门为了传输和加载效率设计现在几乎所有三维引擎都原生支持。而 glTF 恰好又是 Blender 官方原生支持的格式之一。所以答案在这里b3dm 离“能进 Blender 的格式”只差一步就是把内层 glb 拆出来。这步不是“格式转换”而是“拆包”。很多人卡在“b3dm 怎么转模型”这个问题上其实是没意识到它里面已经躺着一个现成的 glb。想通这个概念整条技术路线就顺了b3dm → 提取 glb → Blender 导入 → 单体化 → 导出 FBX。1.2 四段式结构Header FeatureTable BatchTable glbb3dm 文件开头是固定 28 字节的头部Header依次记录 magicb3dm、version、整个文件长度以及后面 Feature Table JSON、Feature Table Binary、Batch Table JSON、Batch Table Binary 四段数据的字节长度。头部之后先排列 Feature Table 和 Batch Table 的元数据段最后才是真正的几何模型 glb。我整理了一个字节偏移表在做脚本提取时可以直接照着写字段起始偏移长度字节说明magic04固定为b3dmversion44版本号通常为 1byteLength84整个 b3dm 文件的总长度featureTableJSONByteLength124Feature Table JSON 段长度featureTableBinaryByteLength164Feature Table Binary 段长度batchTableJSONByteLength204Batch Table JSON 段长度batchTableBinaryByteLength244Batch Table Binary 段长度四段内容28视上面长度而定按 JSON → Binary 顺序排列glb 数据28 四段总长剩余部分真正的模型数据magic 为glTFBatch Table 里通常存的是每个 batch 对应的业务属性比如建筑 ID、楼层数、权属信息。Feature Table 则记录模型整体信息比如 RTC 中心坐标Relative To Center。这些元数据对 Blender 导入没有直接影响真正决定模型长什么样的只有末尾的 glb。1.3 单体化的数据基础batchId在 b3dm 的 Batch Table 里一般会有一组 batchId。这组 ID 给模型里的每个部件编了号比如一批建筑瓦片里1 号是楼 A2 号是楼 B3 号是裙房附属。这个编号会写入 glb 网格的顶点属性_BATCHID或类似属性中。所以单体化并不神秘只要把模型按 batchId 切分成独立对象就实现了逻辑单体化。如果 batchId 在数据生产时丢了那退而求其次按材质分组或者手动分割同样能出效果只是效率低一些。理解了这层关系下面每一步操作就都有根据了。2. 第一步从 b3dm 里提取 glb两条路线2.1 路线对比本地脚本 vs 商业工具做转换前先选路线我见过不少人在这步纠结很久。说下两条路线的实际情况路线 A本地脚本提取 glb再用 Blender 导入导出。优点是完全免费、离线处理、文件不经过第三方服务器大文件不会卡在“上传→转换→下载”的循环里缺点是需要会一点 Python而且如果原始数据带嵌套 Tileset要自己处理多个 b3dm 的拼接与取舍。路线 B用商业 GIS 工具或在线转换平台。优点是界面化操作点点鼠标就能出结果缺点是大文件上传慢、有数据外泄风险、输出格式可能被压缩而且很多工具对 batchId、自定义属性支持不完整转完发现单体化的基础丢了。我的建议很直接只要本地脚本能做的事尽量不依赖在线转换。城市级模型动辄几个 GB上传一次半天回来还不一定保真。用下面这个脚本本地跑一遍所见即所得。2.2 不依赖第三方库的 Python 提取脚本我把脚本贴在这里Python 3.7 以上即可运行不需要安装额外的库。脚本的核心逻辑就是按 1.2 节的字节偏移表读文件头算出四段元数据的总长度然后从偏移位置截到文件末尾就是完整的 glb 数据。import struct import sys def b3dm_to_glb(input_path, output_path): with open(input_path, rb) as f: data f.read() # 固定28字节头部 magic data[0:4] version struct.unpack(I, data[4:8])[0] byte_length struct.unpack(I, data[8:12])[0] ft_json_len struct.unpack(I, data[12:16])[0] ft_bin_len struct.unpack(I, data[16:20])[0] bt_json_len struct.unpack(I, data[20:24])[0] bt_bin_len struct.unpack(I, data[24:28])[0] if magic ! bb3dm: raise ValueError(f{input_path} 不是有效的 b3dm 文件) # 计算 glb 起始位置 start 28 ft_json_len ft_bin_len bt_json_len bt_bin_len glb data[start:byte_length] # 做一次 sanity check确认真的是 glb if glb[0:4] ! bglTF: raise ValueError(f{input_path} 中未找到有效的 glb 数据) with open(output_path, wb) as f: f.write(glb) print(f已提取: {input_path} - {output_path}) if __name__ __main__: if len(sys.argv) ! 3: print(用法: python b3dm_to_glb.py 输入.b3dm 输出.glb) sys.exit(1) b3dm_to_glb(sys.argv[1], sys.argv[2])这个脚本我已经用了很多次处理过大小从几 MB 到几百 MB 的瓦片稳定性没问题。关键校验点就是glb[0:4] ! bglTF这条一旦出现这个报错说明文件本身不完整或者说它不是标准 b3dm。2.3 批量处理一个文件夹内的瓦片一次提取实际项目里 b3dm 往往是一整片区域几十甚至几百个瓦片单个处理不现实。把上面的函数包一层循环配合glob批量扫文件import glob import os if __name__ __main__: for f in glob.glob(*.b3dm): out f.replace(.b3dm, .glb) try: b3dm_to_glb(f, out) except Exception as e: print(f处理 {f} 失败: {e})还有一个小技巧提取出 glb 后确认一遍输出目录中是否有对应的.gltf辅助文件或贴图目录。大部分 b3dm 会将纹理嵌入 glb 内部但少部分建模流程会外链贴图这时 glb 和贴图必须放在同一目录Blender 导入才找得到贴图。3. Blender 4.3 导入 glb 与前期整理3.1 导入参数怎么选打开 Blender 4.3菜单 File → Import → glTF 2.0选中刚才提取的 glb 文件。导入时几个关键点Scale缩放保持默认 1.0。glTF 的 1 单位 1 米和 b3dm 的计量单位一致不要随意改不然比例尺全乱。Forward/Up朝向glTF 是右手系 Y 轴向上Blender 默认世界坐标也是 Y 轴向上直接导入即可不用额外旋转。UV默认会导入如果贴图出现拉伸或黑面通常不是导入问题而是后面材质节点的问题。导入后左侧 Outliner 里会出现一个根节点空物体展开层级可以看到真正的网格对象。这里建议第一时间选中最里层的网格对象把名称改成有意义的名字比如“buildings_root”后续单体化脚本和导出都基于这个对象操作。3.2 坐标、法线、材质体检b3dm 里的模型通常使用局部坐标系物理位置可能离原点很远也可能在原点附近。导入后先在 Object Properties属性面板里看一眼 Location 数值确认它的坐标基准。如果想把整体模型“搬回原点”操作是选中对象 → CtrlA → All Transforms 应用变换然后再用右键 → Set Origin → Origin to Geometry。这一步非常重要尤其是后面要写脚本按 batchId 拆分如果对象带着乱七八糟的父子关系和旋转缩放脚本里取出的顶点坐标会是局部坐标拆出来的单体会错位。我在项目里吃过这个亏合并瓦片时没做 Apply Transform拆完单体后每一栋楼位置全部漂移后来不得不全部回退重来。材质方面倾斜摄影模型一般自带 PBR 贴图导入后会自动生成 Principled BSDF 材质节点。只要 glb 里的纹理没有丢失基本不用改。如果出现紫色材质或黑色材质十有八九是贴图路径找不到了后面第 6 节会专门说排查方法。3.3 把多个瓦片合并成一个整体城市瓦片经常是几十上百个 b3dm提取出来的 glb 也对应几十上百个对象。统一处理时建议把同一区域的网格对象全部选中CtrlJ 合并成一个 Mesh 对象方便后续整体做单体化。合并之前有几个前置动作确认所有对象的缩放是 1、旋转是 0否则合并后坐标基准不一致直接 CtrlJ 会得到一堆错位的三角网。解决办法是逐个选中对象执行 CtrlA → All Transforms。如果合并后出现重复顶点进入编辑模式全选所有顶点按 M 键 → Merge by Distance把重合顶点合并掉。合并阈值默认 0.001m对绝大多数三维瓦片足够。如果贴图在多套 UV 上有冲突合并后要检查 UV 贴图是否正确排列。部分建模软件生成的 UV 布局比较乱Blender 导入时如果自动重排了 UV 岛贴图会错位。这样合并出来的整体后面用 BATCHID 属性拆分时数据基础才干净。4. 单体化操作从一块“大饼”拆成一栋栋房子4.1 单体化的三种思路与适用场景“单体化”这个词在 GIS 行业里有很多解释落到 Blender 操作上其实就是把一个大网格拆成多个独立网格对象。我整理了三种常见思路按适用场景选方案原理适合场景缺点按材质分离不同建筑/地物用了不同材质球按材质分组拆开数据生产时已按地物分类材质差异明显如果整片区域共用一套材质无法拆分按 batchId 分离利用 glb 顶点属性_BATCHID按 ID 把面分组拆开b3dm 带 batchId是最规范的单体化方式依赖原始数据质量ID 缺失时无法用手动 P 键分离人工选中一栋建筑的所有面分离成独立对象少量建筑、ID 缺失、材质混杂效率低大场景不现实4.2 最简单方案按材质分离如果数据生产时已经把不同建筑、不同地物赋予了不同材质单体化简单到只需要三步选中合并后的模型 → 进入编辑模式 → 在面选择模式下按 P 键 → 选择 By Material。Blender 会按材质把网格拆成多个独立对象每个对象自动继承对应材质。这个方案几乎零成本对新手非常友好。但它的局限性也很明显倾斜摄影模型往往只有一个颜色贴图和一张光照贴图整片建筑共用材质按下 By Material 后只会拆出一个对象等于没拆。所以这个方法更适合人工建模的 b3dm不太适合纯倾斜摄影跑出来的连片白模。4.3 最通用方案按 BATCHID 属性拆分成独立对象如果 glb 里带了_BATCHID顶点属性Blender 导入后会把它保留为网格的 Attributes 属性。在 Blender 4.3 里选中网格对象后到属性面板找 Attributes 区域能看到类似BATCHID的条目。也可以用 Python 快速确认import bpy obj bpy.context.active_object if obj.type ! MESH: raise Exception(请选择一个网格对象) print(obj.data.attributes.keys()) for attr in obj.data.attributes: print(attr.name, attr.domain, attr.data_type)输出里如果有_BATCHID/BATCHID/batchid这类属性恭喜单体化的钥匙到手了。接下来按 batchId 分组把相同 ID 的面归到同一个新对象里。我提供一个统计和验证用的短脚本跑完能告诉你模型里到底有几个单体、每个单体有多少面import bpy from collections import defaultdict obj bpy.context.active_object mesh obj.data attr_name None for name in (_BATCHID, BATCHID, batchid, _batchid): if name in mesh.attributes: attr_name name break if attr_name is None: print(未找到 BATCHID 属性无法按 ID 单体化) else: attr mesh.attributes[attr_name] face_groups defaultdict(int) for face in mesh.polygons: # 取该面第一个顶点的 batchId 作为归属 bid int(attr.data[face.vertices[0]].value) face_groups[bid] 1 print(f找到 {len(face_groups)} 个单体ID:) for bid, count in sorted(face_groups.items()): print(f ID {bid}: {count} 个面)这个脚本不直接执行拆分但帮你判断数据是否值得拆、拆出来有多少个物体。我自己处理过一批瓦片用这个脚本一跑发现 batchId 有几百个但很多 ID 只挂了十几个面属于噪声碎块。这种情况拆分后还要做一次“空壳子清理”把面数极少的对象删掉或合并回最近的主建筑否则导出的 FBX 会被碎块淹没下游系统根本没法看。至于真正执行拆分的脚本我在实际项目中很少用全自动因为数据量大时 Blender 的 bmesh 操作很容易崩溃而且 ID 噪声会导致拆出一堆垃圾物体。更稳的做法是用脚本统计 ID然后回到编辑模式按 batchId 把对应面选中按 P 键 → Separate by Selection。虽然听起来“半自动”但实际效率远高于全自动脚本踩坑后返工。你可以在编辑模式下配合 Select Pattern按属性值选面来加速Blender 4.3 对属性的选择支持已经比较成熟。4.4 手动 P 键分离没有 batchId、材质又混杂的情况就只能手动了。操作流程选中模型 → 进入编辑模式 → 切换到面选择模式 → 选中一栋楼的外轮廓顶面或者从任意一个面开始按 L 键选中相连面→ 如果漏选了按住 Shift 继续加选 → 确认选完所有属于这栋楼的面 → 按 P 键 → Separate by Selection。分离后回到对象模式新生成的对象还叫老名字的一部分建议立即重命名为“building_001”这样的规范编号。手动操作适合单体数量在几十以内的小场景如果整片区域有几千栋楼还指望手动拆那我劝你回到数据生产源头去拿原始建模文件别在 Blender 里自己硬扛。5. 导出 FBX 与坐标、材质注意事项5.1 FBX 导出参数单体化完成、每个建筑都成为独立对象后就到了导出 FBX 环节。操作路径全选需要导出的对象 → File → Export → FBX (.fbx)。几个参数直接决定下游能不能正常打开Scale保持 1.0。FBX 和 Blender 的单位换算在这里非常容易踩坑如果改成了 0.01导出的模型会缩小 100 倍。Apply Scalings建议选 All Local把对象自身的缩放应用到网格数据上避免下游软件看到一堆非 1 的缩放值。Forward / Up如果下游是 Unity一般用 Forward -Z、Up Y如果是 Unreal Engine一般用 Forward X、Up Z如果是不确定选 Forward -Z、Up Y 最通用。Path Mode一定选 Copy这样贴图会复制到导出文件同目录不选的话下游打开 FBX 会提示贴图丢失。勾选 Embed Textures 可以把纹理直接打包进 FBX省去给下游传贴图目录的麻烦缺点是文件体积会明显变大。能接受大一点文件的话我建议勾上省心。5.2 材质怎么保真FBX 对 PBR 材质的支持没有自己吹的那么完美。Blender 导出 FBX 时Principled BSDF 的基本属性可以转过去但复杂的材质节点树多层贴图混合、程序化纹理、Mask 遮罩大概率会打折。如果你在单体化之后调整过材质导出前最好检查每个材质球是否只是“颜色贴图 法线贴图 金属度/粗糙度贴图”这种基础组合。我自己遇到最多的坑是在 Blender 里加了大量程序化节点调色导出 FBX 后颜色全乱。解决办法是把材质“拍平”即单独烘焙一版最终颜色贴图再赋给模型。对于城市模型这种以展示为主的资产烘焙 Base Color 和 Roughness 两张图基本就够用了。5.3 坐标和比例尺还准不准单体化后的每个对象如果保留了世界坐标FBX 导出后位置关系不会乱。但要注意FBX 是 Y 轴向上的如果下游工具是 Z 轴向上常见于建筑 BIM 软件导入下游后必须设置一次旋转校正或者在 Blender 导出时把 Up 轴改成 Z。另外很多 b3dm 使用局部坐标系模型在 Blender 里看起来正好落在原点附近但如果原始瓦片使用了全局投影坐标导入后的坐标数值可能非常大。此时导出的 FBX 坐标也会是几千上万米的数值下游 GIS 平台可能因为浮点精度问题产生抖动。稳妥做法是在 Blender 里把模型移动到原点附近同时记录一个偏移量导出后在 GIS 平台里再平移到正确位置。这样可以保留比例尺同时避免大量级浮点误差。6. 实操中踩过的坑和排查方法6.1 提取出来的 glb 损坏脚本报不是有效的 b3dm 文件或未找到有效的 glb 数据最常见原因是文件本身不是标准 b3dm可能是 3D Tiles 1.1 的新变种也可能是别的瓦片格式改了后缀。解决办法用十六进制编辑器打开文件头看前 4 字节到底是b3dm还是pnts还是cmpt。pnts是点云cmpt是复合容器处理逻辑完全不同。遇到cmpt说明里面还包了多个子瓦片需要先解包复合体再提取。6.2 贴图丢失、全黑glb 导入后模型变黑90% 是纹理没跟上。先看 Shader Editor 里的 Image Texture 节点图片路径是否有效如果是空路径手动找到同目录贴图文件重新指定即可。另一种情况是贴图嵌在 b3dm 里但提取 glb 时脚本截取范围不对导致材质引用了错误的子资源。可以把 glb 重新拖进 Blender 看看如果还是黑就用 glTF Validator 之类工具检查 glb 是否完整。6.3 batchId 属性找不到在 Blender 的 Attributes 区域没看到 BATCHID不代表数据里没有。有些导入流程会把 batchId 转换成顶点颜色信息或者存成 UV 通道。你可以在 Python 里把 mesh.attributes 全部打印出来一个个确认如果确实没有再看 batchTable 存的是不是属性表而非顶点属性。总之一句话b3dm 里不是一定有 batchId取决于生产软件和数据规范。6.4 文件太大Blender 卡死城市级模型动辄几百万面导入后转一下视角都掉帧。这里分享两个好用的优化手段导出前做减面在 Blender 里用 Decimate 修改器卡到 1/2 或 1/3 面数对远处楼群来说肉眼几乎无差别。用实例化代替复制单体化后的对象如果有很多重复楼栋可以把重复的楼做成 Collection Instance而不是复制几千份网格这样 Blender 和 FBX 体积都会大幅下降。6.5 分离后部件错位按 batchId 分离后出现位置漂移几乎都是原点问题或者坐标基准问题。分离后的每个新对象会自动生成独立的 Local Origin如果原始网格做了若干次变换没有 ApplyOrigin 和世界坐标就对不上。回到第 3.2 节说的合并对象之前务必 CtrlA 应用所有变换这一步能避免一半以上的错位问题。6.6 问题速查表现象可能原因排查/解决办法脚本报 magic 错误文件不是 b3dm用十六进制查看器确认文件头提取后 glb 无法导入数据损坏或切片范围不对检查输出 glb 是否以glTF开头模型全黑贴图路径丢失重新指定 Image Texture 路径BATCHID 属性消失导入器未识别自定义属性查 Attributes 列表、或检查 batchTable 是否在元数据里拆完单体位置飘变换未 Apply合并前 CtrlA → All TransformsFBX 导入下游后缩小导出 Scale 不对确认 Scale1.0、Apply ScalingsAll Local最后再啰嗦两句实际体会这条路走了几次之后我最深的感受是b3dm 转 FBX 本身不难难的是单体化的“度”怎么把握。如果原始数据质量好batchId 齐全脚本一跑几十栋楼整整齐齐拆出来导出的 FBX 直接进 Unity 做点击高亮效果非常理想如果数据是从倾斜摄影直接跑的batchId 基本等于没有材质也全是一套这时候更值得关注的是“把楼群按空间切块”而不是追求逐栋单体。还有一个建议不管多赶工期单体化后的对象一定要做规范化命名。至少包含“建筑编号_楼层_用途”三个属性对后续归档、交接、下游系统接入省下的时间远超命名那几分钟。另外大面积模型建议按批次导出不要一个 Blender 文件塞几百个瓦片导出到一半崩溃是最让人崩溃的。我自己现在遇到类似需求通常会先用 2.2 节的脚本把 b3dm 批量换成 glb再进 Blender 做一次体格检查确认 batchId 和材质情况后才决定走哪一条单体化路线。大部分项目到导出 FBX 这一步就结束了偶尔需要在 Blender 里再补一点光影和细节那又是另一个话题了这篇文章写到这路已经铺好剩下就看你手里的数据质量了。