☰
b3dm转FBX全流程:Blender单体化实战指南(含glb提取脚本)
2026/10/18 6:41:27 网站建设 项目流程

先说个场景。项目里拿到一批城市级三维瓦片,后缀是.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 + glb

b3dm 文件开头是固定 28 字节的头部(Header),依次记录 magic('b3dm')、version、整个文件长度,以及后面 Feature Table JSON、Feature Table Binary、Batch Table JSON、Batch Table Binary 四段数据的字节长度。头部之后先排列 Feature Table 和 Batch Table 的元数据段,最后才是真正的几何模型 glb。

我整理了一个字节偏移表,在做脚本提取时可以直接照着写:

字段起始偏移长度(字节)说明
magic04固定为b3dm
version44版本号,通常为 1
byteLength84整个 b3dm 文件的总长度
featureTableJSONByteLength124Feature Table JSON 段长度
featureTableBinaryByteLength164Feature Table Binary 段长度
batchTableJSONByteLength204Batch Table JSON 段长度
batchTableBinaryByteLength244Batch Table Binary 段长度
四段内容28视上面长度而定按 JSON → Binary 顺序排列
glb 数据28 + 四段总长剩余部分真正的模型数据,magic 为glTF

Batch Table 里通常存的是每个 batch 对应的业务属性,比如建筑 ID、楼层数、权属信息。Feature Table 则记录模型整体信息,比如 RTC 中心坐标(Relative To Center)。这些元数据对 Blender 导入没有直接影响,真正决定模型长什么样的,只有末尾的 glb。

1.3 单体化的数据基础:batchId

在 b3dm 的 Batch Table 里,一般会有一组 batchId。这组 ID 给模型里的每个部件编了号,比如一批建筑瓦片里,1 号是楼 A,2 号是楼 B,3 号是裙房附属。这个编号会写入 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 != b'b3dm': 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] != b'glTF': 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] != b'glTF'这条,一旦出现这个报错,说明文件本身不完整,或者说它不是标准 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 数值,确认它的坐标基准。如果想把整体模型“搬回原点”,操作是:选中对象 → Ctrl+A → All Transforms 应用变换,然后再用右键 → Set Origin → Origin to Geometry。

这一步非常重要,尤其是后面要写脚本按 batchId 拆分,如果对象带着乱七八糟的父子关系和旋转缩放,脚本里取出的顶点坐标会是局部坐标,拆出来的单体会错位。我在项目里吃过这个亏:合并瓦片时没做 Apply Transform,拆完单体后每一栋楼位置全部漂移,后来不得不全部回退重来。

材质方面,倾斜摄影模型一般自带 PBR 贴图,导入后会自动生成 Principled BSDF 材质节点。只要 glb 里的纹理没有丢失,基本不用改。如果出现紫色材质或黑色材质,十有八九是贴图路径找不到了,后面第 6 节会专门说排查方法。

3.3 把多个瓦片合并成一个整体

城市瓦片经常是几十上百个 b3dm,提取出来的 glb 也对应几十上百个对象。统一处理时,建议把同一区域的网格对象全部选中,Ctrl+J 合并成一个 Mesh 对象,方便后续整体做单体化。

合并之前有几个前置动作:

  • 确认所有对象的缩放是 1、旋转是 0,否则合并后坐标基准不一致,直接 Ctrl+J 会得到一堆错位的三角网。解决办法是逐个选中对象执行 Ctrl+A → 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,如果原始网格做了若干次变换没有 Apply,Origin 和世界坐标就对不上。回到第 3.2 节说的,合并对象之前务必 Ctrl+A 应用所有变换,这一步能避免一半以上的错位问题。

6.6 问题速查表

现象可能原因排查/解决办法
脚本报 magic 错误文件不是 b3dm用十六进制查看器确认文件头
提取后 glb 无法导入数据损坏或切片范围不对检查输出 glb 是否以glTF开头
模型全黑贴图路径丢失重新指定 Image Texture 路径
BATCHID 属性消失导入器未识别自定义属性查 Attributes 列表、或检查 batchTable 是否在元数据里
拆完单体位置飘变换未 Apply合并前 Ctrl+A → All Transforms
FBX 导入下游后缩小导出 Scale 不对确认 Scale=1.0、Apply Scalings=All Local

最后再啰嗦两句实际体会

这条路走了几次之后,我最深的感受是:b3dm 转 FBX 本身不难,难的是单体化的“度”怎么把握。如果原始数据质量好,batchId 齐全,脚本一跑,几十栋楼整整齐齐拆出来,导出的 FBX 直接进 Unity 做点击高亮,效果非常理想;如果数据是从倾斜摄影直接跑的,batchId 基本等于没有,材质也全是一套,这时候更值得关注的是“把楼群按空间切块”,而不是追求逐栋单体。

还有一个建议:不管多赶工期,单体化后的对象一定要做规范化命名。至少包含“建筑编号_楼层_用途”三个属性,对后续归档、交接、下游系统接入省下的时间远超命名那几分钟。另外,大面积模型建议按批次导出,不要一个 Blender 文件塞几百个瓦片,导出到一半崩溃是最让人崩溃的。

我自己现在遇到类似需求,通常会先用 2.2 节的脚本把 b3dm 批量换成 glb,再进 Blender 做一次体格检查,确认 batchId 和材质情况后才决定走哪一条单体化路线。大部分项目到导出 FBX 这一步就结束了,偶尔需要在 Blender 里再补一点光影和细节,那又是另一个话题了,这篇文章写到这,路已经铺好,剩下就看你手里的数据质量了。

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

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

立即咨询