前阵子有朋友丢给我一整套三维瓦片,说是某个街区的高精度倾斜摄影成果,文件后缀清一色是.b3dm。他在三维GIS平台里看得很爽,可美术那边要的是 FBX,还要求在 Blender 4.3 里把每栋楼单独拎出来——也就是常说的单体化。这条链路听起来不复杂,真走一遍你就会发现:拆包、格式转换、坐标修正、单体化导出,每一步都埋着雷。这篇就把完整流程过一遍,从 b3dm 结构讲到 FBX 导出,重点放在 Blender 4.3 里的模型体检和单体化操作,最后给出一些我实际踩坑后的建议。
1. b3dm 这个容器到底装了什么,为什么绕不开 FBX
很多刚从 GIS 转过来做三维的朋友,看到 b3dm 第一反应是"这跟 glTF 是不是一回事"。严格说,b3dm 是 3D Tiles 标准里的一种瓦片容器,全称 Batched 3D Model,它并不是一种纯粹的模型格式。理解这一点,后面所有转换逻辑就顺了。
1.1 拆开 b3dm 的封装:几何藏在 GLB 里,属性挂在 batchTable 上
b3dm 更像是一个快递盒:外壳是 28 字节的二进制头,里面装着一个完整的 GLB 文件,盒子侧面还贴了标签,叫 featureTable 和 batchTable。GLB 是 glTF 的二进制形态,几何网格、材质、贴图、UV、骨架动画全都能塞进一个文件里,便于快速加载。倾斜摄影生成的高精度三角网,几乎都是靠 GLB 承载的。
batchTable 则负责记录"建筑材料属性",比如建筑物 ID、高度、层数、面积,甚至原始属性表的 JSON。这些属性的用途很关键:如果你走的是 ID 单体化或属性单体化,batchId 就是区分每一栋建筑的钥匙。但问题在于,Blender 默认导入 GLB 时,并不主动解析 b3dm 的 batchTable,所以单体化时我们不能指望"导入后自动有对象 ID",这一点后面会详细说。
28 字节 b3dm 头的字段顺序如下:
偏移 长度 说明 0 4 magic,固定 "b3dm" 4 4 version,目前是 1 8 4 byteLength,整个 b3dm 文件字节数 12 4 featureTableJSONByteLength 16 4 featureTableBinaryByteLength 20 4 batchTableJSONByteLength 24 4 batchTableBinaryByteLength看到这里你应该明白了:任何声称能从 b3dm 直接读模型的工具,本质上做的都是"把头剥掉,把里面的 GLB 取出来"。下面用 Python 标准库就能完成这一步:
import json import struct from pathlib import Path def b3dm_to_glb(input_path: Path, output_path: Path) -> None: data = input_path.read_bytes() if data[:4] != b"b3dm": raise ValueError("不是有效的 b3dm 文件") byte_length = struct.unpack_from("<I", data, 8)[0] ft_json_len = struct.unpack_from("<I", data, 12)[0] ft_bin_len = struct.unpack_from("<I", data, 16)[0] bt_json_len = struct.unpack_from("<I", data, 20)[0] bt_bin_len = struct.unpack_from("<I", data, 24)[0] start = 28 + ft_json_len + ft_bin_len + bt_json_len + bt_bin_len glb = data[start:byte_length] if glb[:4] != b"glTF": raise ValueError("b3dm 中没有找到内嵌 GLB") output_path.write_bytes(glb) # 批量转换目录下所有 b3dm tiles_dir = Path("./tiles") out_dir = Path("./glb_out") out_dir.mkdir(exist_ok=True) for b3dm_file in tiles_dir.glob("*.b3dm"): b3dm_to_glb(b3dm_file, out_dir / (b3dm_file.stem + ".glb"))这一段脚本是整条流水线的地基。实际处理几百个瓦片时,我一般先跑一遍,检查有没有文件头解析失败的情况——遇到过某些二次开发的软件会在 b3dm 头里塞扩展字段,导致直接偏移出错,这时候需要在命令行里打印 header 信息逐字节核对。
1.2 为什么最终目标偏偏是 FBX
同样的模型,转 OBJ 也行、转 glTF 也行,为什么大家都指定 FBX?核心原因在于下游生态。FBX 是 Autodesk 体系下的通用交换格式,Unity、Unreal、3ds Max、Maya、Blender 全都有成熟的原生支持。
OBJ 虽然通用,但只能存最基础的几何和贴图路径,材质节点、物体层级、坐标轴约定这些信息基本全丢。glTF 在 Web 3D 和游戏引擎里确实更现代,但它对 DCC 工具(数字内容创作工具)的适配目前还赶不上 FBX,尤其是在动画分层、单位换算、命名空间保留这些细节上。所以当一个项目需要跨软件交接时,FBX 几乎是最稳妥的中间格式。
这里要补充一个反直觉的结论:如果你手里已经拿到了 GLB,我通常不建议再单独用命令行工具转一次 FBX 再导入 Blender。Blender 4.3 对 glTF/GLB 的原生导入已经非常成熟,直接导入 GLB 再导出 FBX,材质、UV、法线的保留率远高于中间经过一次通用转换器。换句话说,标题里的"转为 FBX"放到整个流程的末端执行,中间先用 GLB,是更省心的做法。
2. 转换路线选型,别一上来就找一键工具
很多人上来就想找一个"b3dm 转 FBX"的一键工具,但这类工具往往存在两个问题:一是对 b3dm 的 batchTable 支持很弱,建筑属性在转换后就丢了;二是自动生成的材质常常不符合下游软件预期,导入 Blender 后还要重新指定贴图。所以与其找万能转换器,不如先想清楚你要的是什么。
2.1 三条主路线的对比
我把实际操作中常见的方案列成一张表,方便你根据项目情况选:
| 路线 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| A. Blender 中转 | 拆出 GLB → Blender 导入 GLB → 处理完再导出 FBX | 材质保留最好,能顺手做单体化和修复,流程可控 | 需要手动操作,批量场景要写脚本 |
| B. 通用转换器 | 拆出 GLB → 转成 FBX → 再导入 Blender | 适合纯批量流水线,速度快 | 材质节点容易丢,法线可能翻转,单位容易错 |
| C. GIS 工具箱 | 直接用集成工具导出 FBX | 操作门槛低 | 依赖具体版本,单体化能力弱,属性与网格的映射关系容易丢 |
路线 B 对单个文件确实很快,但当模型面数动辄几百万、纹理贴图还带透明通道时,通用转换器生成的 FBX 往往会出现一堆材质球,命名混乱,导入 Blender 后还得重新整理,省下的时间又赔回去了。所以只要不是一次性处理几百上千个文件,我推荐路线 A。
2.2 我实际采用的组合
这次项目里有差不多 200 个 b3dm 瓦片,但我只需要把其中重点区域合并成一个场景来做单体化。我的做法是:
- 先用上一章的 Python 脚本把所有 b3dm 批量拆成 GLB;
- 用 Blender 4.3 的 File > Import > glTF 2.0 导入全部 GLB;
- 在 Blender 里合并场景、修正坐标、做单体化、导出 FBX。
有些瓦片属于相邻区域,导入后会在同一个世界坐标下自动拼接。只要原数据的坐标系一致,Blender 里导入后它们天然就在正确相对位置——这也是 GLB 导入的优势之一。关键提醒:导入前先在 Blender 的 Scene Properties 里把单位设为米,这决定了之后 FBX 导出的比例是否错乱。
3. 拆包之后:GLB 到 FBX 的两种通道
当你已经拿到 GLB 文件,下一步无非两条路:要么先转成 FBX,要么直接进 Blender。这里把两条路都讲清楚,免得你在不同场景下走弯路。
3.1 通道一:直接在 Blender 里导成 FBX
打开 Blender 4.3,File > Import > glTF 2.0,选中 GLB 后默认参数导入即可。场景里会生成一个或多个网格对象,材质节点会自动创建 Principled BSDF 和对应的 Image Texture 节点。这时候什么都不用改,直接 File > Export > FBX,选好输出路径,就能得到 FBX。
这个通道的隐藏价值在"二次处理":你可以在导入后顺手把多余的相机、空物体删掉,把法线重算一遍,再导出。这些操作虽然不起眼,却能避免 FBX 在别的软件里偏色、变黑或出现异物。注意导出 FBX 的默认参数,Forward 为 -Z、Up 为 Y,这套坐标约定正好匹配 Unity 和 Unreal,如果下游是 3ds Max,建议改成 Forward: -Y、Up: Z。
3.2 通道二:用命令行工具批量直转 FBX
如果你手头有上百个 GLB 要快速转 FBX,逐个打开 Blender 显然不现实。这种场景下,我建议用脚本方式批量处理:
import bpy import sys glb_dir = sys.argv[-2] fbx_dir = sys.argv[-1] for glb_file in sorted(glb_dir.glob("*.glb")): bpy.ops.wm.read_factory_settings() bpy.ops.import_scene.gltf(filepath=str(glb_file)) fbx_name = glb_file.stem + ".fbx" bpy.ops.export_scene.fbx( filepath=str(fbx_dir / fbx_name), path_mode="COPY", embed_textures=True, use_mesh_modifiers=True, apply_scale_options="FBX_SCALE_UNITS", )这里有两个参数很关键:embed_textures=True会把贴图直接嵌进 FBX 文件里,避免交付后贴图路径失效;path_mode="COPY"则是把依赖文件复制到导出目录。如果你在 Linux 服务器上没有界面环境,可以用如下命令跑:
blender -b -P batch_glb_to_fbx.py -- ./glb_dir ./fbx_dir注意blender -b是后台模式,不会弹出任何窗口,非常适合做批量流水线。最后得到的 FBX 已经是 Blender 处理过的,材质、UV、坐标都不会有常见转换工具那么明显的损失。
4. 导入 Blender 4.3 之后,先别急着做单体化
真正导入模型后,80% 的精力都花在"体检"上。倾斜摄影模型不像手工建模,它有非常典型的 GIS 特性:坐标值巨大、单位可能是米也可能是度、网格存在大量冗余顶点、法线方向可能不统一。这些不解决,单体化做着做着就会出现"选不中想要的建筑"或者"分离后建筑一半黑脸"的诡异现象。
4.1 坐标"飞点"问题:模型跑到几公里外怎么办
b3dm 里的坐标通常来自投影坐标系或局部 ENU(东-北-上)坐标系。偏转后的坐标数值可能非常大,比如 X 接近 500000 米。Blender 的网格顶点位置虽然支持双精度,但很多操作和渲染管线依然基于单精度浮点,一旦坐标太大,模型会出现顶点抖动、雕刻效果差、选择面时高亮显示混乱等问题。
解决办法是"整体挪回原点"。选中所有对象后,进入编辑模式,全选顶点,在侧边栏的 Transform 面板里手动把 X、Y 减去一个固定偏移量。更推荐的做法是建立一个 Empty 作为父级,把偏移量记在 Empty 上,网格对象保持相对坐标。这样既保住了原始地理参考信息,又让 Blender 操作顺畅。无论哪种方法,记住偏移量,导出 FBX 前或导入其他软件时再加回去。
4.2 单位之坑:FBX 的厘米和 Blender 的米
这是所有跨格式流程里最容易踩、且后果最迷惑的坑。glTF 标准规定单位是米,所以 GLB 直接导入 Blender 后 1 单位就是 1 米,很省心。但 FBX 的历史包袱是 Autodesk 单位,默认按厘米处理。如果你用命令行工具提前转了一次 FBX,再导入 Blender 时就会看到模型被放大了 100 倍。
在 Blender 4.3 的 FBX 导入面板里,有两个相关选项:Scale 和 Global Scale。遇到放大 100 倍的情况,把 Global Scale 设为 0.01 即可恢复真实比例。但我更推荐你在流程里尽量让 GLB 作为"真源",用 Blender 导出 FBX 时在导出面板选择Scale: FBX Units Scale,这样导出的 FBX 会自动把 1 米映射为 100 厘米,下游软件拿到的实际尺寸不会变。
4.3 材质与 UV 的检查清单
导入 GLB 后,先别急着关掉 Shader Editor。倾斜摄影模型的纹理一般是一张或几张大的图集(Texture Atlas),整个场景共享 UV 空间。这种纹理烘焙方式很常见,所以单体化时不要随便重新展开 UV,否则一张图集就会被拆得七零八落。
检查清单如下:
- 材质球数量是否合理?如果出现几十个同名的材质球,需要考虑是否导入时把每栋楼拆成了独立对象。
- 在 Shader Editor 里确认 Principled BSDF 有正确的 Base Color 输入,而不是一张加载失败的纹理节点。
- 如果模型显示为全灰,去 Build 里看 Image Texture 节点的路径是否还在。GLB 通常是嵌入纹理,一般不会丢,但一旦走了"b3dm → GLB → 转换工具 → FBX"这条链路,贴图路径就很容易失效。
- 有透明区域(比如玻璃幕墙)的贴图,检查 Alpha 通道是否被识别,如果纯黑或纯白,说明 Blending 模式和 Alpha 节点没有接对。
做完这些检查,再进入编辑模式,全选网格,按Shift+N重算法线。倾斜摄影数据经常出现法线翻转,这一步能避免后面单体化分离出的建筑"一面正常一面全黑"。
4.4 合并顶点和清理杂散面
倾斜摄影 Mesh 是由大量三角面拼出来的,顶点共享关系不一定健康。导入后在高密度区域可能出现面重叠、缝隙、孤立顶点。用Merge by Distance,距离阈值设在 0.001 米左右,能有效合并重叠顶点。注意距离阈值不要设太大,否则会把相邻建筑的顶点也焊在一起,反而不利于单体化。
清理完顶点后,用Mesh > Clean Up > Delete Loose删掉孤立顶点,用Limited Dissolve合并共面三角形。这两个操作对整体面数有很明显的优化效果,对单体化选面的帮助也很大——面片越少,Ctrl+L选相连面的准确率越高。
5. 单体化落地:从一整块三角网到一栋栋独立对象
单体化这个词在三维 GIS 里常被挂在嘴边,但不同语境下指的东西不一样。我见过有人把单体化理解成"画个框高亮一下",也有人把它理解成"把模型切割成一个个独立文件"。在 Blender 里我们实际做的是后者,也就是几何单体化:让每栋建筑成为一个可以独立选中、编辑、导出的网格对象。
5.1 三种单体化思路,Blender 能做的是哪一种
表格里做个对比:
| 单体化方式 | 原理 | 适合场景 | Blender 可行度 |
|---|---|---|---|
| 切割单体化 | 把属于同一建筑的三角面物理分割成独立对象 | 需要独立编辑、导出模型 | 可行,本文讲这种 |
| 属性单体化 | 给每个三角面写入建筑 ID,模型不切割 | 保留原始 Mesh 完整性,做属性查询 | 部分可行,需要扩展插件配合 |
| ID 单体化 | 用透明辅助模型贴合建筑轮廓,点击时关联属性 | WebGIS 平台做点击高亮 | 不适合纯 Blender 流程,需回到 GIS 平台 |
大多数非 GIS 项目要的其实是切割单体化。因为美术不在乎你能不能点选,他们只想把某栋楼拖进引擎里单独修改。所以本文的"单体化"均指在 Blender 中把建筑网格分离成独立对象。
5.2 手动选面:一个建筑一个建筑地挑出来
导入并修好模型后,进入编辑模式。在右上角的 Viewport 材质预览里打开带纹理的显示模式,这时候你能清楚看到一栋栋建筑的轮廓。选择建筑顶部任意一个面,然后按Ctrl+L(Select Linked)就能选中与它相连的整个三角网区域。
这个方法对付孤立的建筑基本没问题。但如果建筑之间通过墙面、廊道、地面共用顶点,或者建筑底层是连通的商业裙房,Ctrl+L会把好几栋楼一起选中。这类情况需要你先在编辑模式下把连接处"切断":找到相邻建筑之间的过渡面,比如屋檐下的连廊,手动选中这些面,按X删除,仅删面不删顶点,破坏连通性后再重新选。
这里有一个实用技巧:选择建筑的大致范围时,先用矩形框选(B 键)框出建筑所在区域,再使用Ctrl+L去除误选的其他部分。倾斜摄影网格三角面密度极高,纯手动逐个点选不现实,框选加联动选择能省下很多时间。
5.3 分离、命名、加属性:三步整理输出对象
选中一个建筑的完整网格后,按P键,选择"Selection"分离。此时这个建筑会从原场景中独立出来,成为一个新对象。重复上述操作,把所有需要单体化的建筑都分离出来。
分离出来的对象需要做三件琐碎但重要的事:
- 重命名:直接在 Outliner 里把对象名改成
building_001、building_002这种规范的名字。如果数量很多,用 Blender 的 Batch Rename 插件批量处理。 - 设置原点:选中对象,右键 > Set Origin > Origin to Geometry。这样对象原点会落在几何重心,方便后续在引擎里摆放和旋转。
- 添加自定义属性:在 Object Properties 面板最下方的 Custom Properties 里,添加键值对,比如
height: 32.5、area: 1200、name: 社区服务中心。这些属性在导出 FBX 后大部分能被 Unity/Unreal 识别,作为物体元数据保留下来。
如果你有 batchTable 转出的 CSV 或 JSON 属性表,可以在这一步按建筑 ID 手动或脚本化地把属性写进 Custom Properties。属性数值不要求多全,能把建筑名称、层数、高度记录上,交付时就能省掉大量口头沟通。
5.4 屋顶和底面的处理
倾斜摄影单体化后,一般建筑底部是没有底面的——它和地面共用了网格,分离时自然把底面留在了原场景。很多时候这无所谓(放在地面上本来就看不见底面),但如果要求模型在水面上或建筑需要单独展示,就得补一个底面。
补底面最快的方法是:选中建筑底部边缘一整圈边界边(Shift 键逐个选择或按G配合吸附),然后用菜单Face > Grid Fill生成网格封面。Grid Fill 对复杂轮廓生成的网格不一定漂亮,但对大多数建筑来说,一个平面已经够用。用Alt+M合并顶点到中心也能做三角扇形的底面,效果因人而异。
如果你想保持干净,也可以按F键把边缘选中后直接填充一个 N-Gon 面。Blender 4.3 对 N-Gon 的处理很成熟,导出游戏引擎时会自动转成三角面,不影响使用。
6. 导出 FBX 的参数细节和交付后最常见的三个问题
单体化完成后,最后一步是把整理好的建筑导出成 FBX。这一步的参数如果不注意,前面所有努力会在导出瞬间被"打回原形"。
6.1 导出面板推荐配置
选中要导出的建筑对象,File > Export > FBX,我推荐的配置如下:
| 参数 | 设置 | 理由 |
|---|---|---|
| Include | Limit to Selected Objects | 只导出选中的建筑,避免把地皮和大场景一起塞给下游 |
| Object Types | Mesh | 排除空物体、相机、灯光等无关对象 |
| Apply Scalings | FBX Units Scale | 保持 1 单位 = 1 米,下游软件按 Autodesk 单位读取时不会差 100 倍 |
| Forward / Up | -Z / Y | 匹配 Unity 和 Unreal 的默认坐标习惯 |
| Apply Transform | 开启 | 消除对象级负缩放或旋转带来的隐患 |
| Path Mode | Copy | 把贴图复制到 FBX 同级目录 |
| Embed Textures | 开启 | 把纹理直接嵌入 FBX,方便分发单个文件 |
| Use Mesh Modifiers | 开启 | 如果有细分或清理修改器,导出时应用效果 |
其中 Embed Textures 这个开关,我强烈建议交付外部团队时打开。虽然文件体积会变大,但对方拿到 FBX 后不需要手动找贴图,省下大量沟通成本。如果你做的是内部资产,贴图统一走共享路径,那么 Path Mode 选 Relative 也行。
6.2 交付后常见问题排查
每次导出交付后,总会收到类似问题。我总结三个高频的,附上排查思路:
- 模型方向不对,建筑躺在地上歪着立起来。这通常是导出面板的 Forward/Up 设置和下游软件不一致。Unity/Unreal 默认用 -Z 作为前、Y 作为上,Blender 默认也是这套;但如果下游是 3ds Max,需要改为 Forward: -Y、Up: Z。出问题先问对方软件是什么,再对应调整。
- 材质显示为紫红色或全灰。检查 FBX 同级目录是否有贴图文件。右键缺失纹理的材质球,在 Image Texture 节点里手动重新指定路径即可。如果对方只有单个 FBX 文件,那就要怪我没有开 Embed Textures。
- 导入后尺寸巨大或缩成一点。Quick 检查在 Blender 里物体的实际大小:选中对象,查看 Dimensions 属性。如果原比例是 1:1,说明对方软件的单位设置和 FBX 不一致,设置对方软件单位为厘米或米即可。
6.3 批量导出:几十个建筑逐个导太慢,用脚本
如果单体化出几十栋楼,不想一个个导出,可以写一个 Blender Python 脚本,在后台模式下自动遍历所有对象并导出:
import bpy import pathlib out_dir = pathlib.Path("/output/fbx_buildings") out_dir.mkdir(parents=True, exist_ok=True) for obj in bpy.data.objects: if obj.type != "MESH" or not obj.name.startswith("building_"): continue # 先选中,再进入导出 bpy.ops.object.select_all(action="DESELECT") obj.select_set(True) bpy.context.view_layer.objects.active = obj export_path = str(out_dir / (obj.name + ".fbx")) bpy.ops.export_scene.fbx( filepath=export_path, use_selection=True, apply_scale_options="FBX_SCALE_UNITS", path_mode="COPY", embed_textures=True, )跑法依然是blender -b -P batch_export_fbx.py。这样每个建筑一个 FBX 文件,文件名和对象名一致,后续想要在引擎里跟属性表关联也方便。
6.4 一根筋流程的代价:为什么我不推荐先转 FBX 再导入 Blender
最后聊一个纯经验层面的建议。不少人看到"b3dm 转 FBX"的步骤,会直接搜转换工具,转完再拉进 Blender,然后发现模型不是大了 100 倍就是贴图全丢。这套流程兜兜转转,每一步失败的判定成本都很高。与其这样,不如接受"Blender 中转"的思路:b3dm 拆成 GLB,Blender 直接读 GLB,把所有修复和单体化做完,最后在导出节点统一输出 FBX。
如果某个同事或客户认死理,非要一个"中间 FBX"文件,那就在 Blender 里导入 GLB 后立即再导出 FBX,再回头把 FBX 导入检查一遍。这个中间 FBX 的质量,比任何命令行转换库出来的都更可控。
我在实际操作中发现,单体化最耗时间的从来不是格式转换,而是选面和清理几何。所以如果你不想在这上面花太多时间,不妨先把原始模型的共享顶点合并到最小可接受程度,再用框选加Ctrl+L的方式分离建筑。做几个项目后,你会发现一套稳定的处理顺序比任何工具都重要:拆包 → 导入 → 修坐标 → 清几何 → 单体化 → 导出。每个环节都用自己的经验校准一遍,这条流水线才算真正跑通。