批量转换模型格式这件事,通常会被当成苦力活。尤其当目录里躺着几百个来自不同建模工具的FBX、OBJ或者glTF文件时,手动打开Blender、导入、再导出,不仅慢,还容易在轴系、缩放和材质上出错。Blender内置的Python解释器可以直接调用bpy模块,把重复动作交给无头模式执行。接下来的方案基于Blender 3.x/4.x,Windows、macOS和Linux通用。核心思路是遍历源目录,按扩展名选择对应导入器;修正场景单位、轴向后导出成目标格式;最后输出统计结果。

先搞清楚三种格式在转换中的关键差异
FBX是Autodesk的交换格式,擅长保留骨骼、动画、材质节点和层级关系,但不同软件对FBX版本和轴系处理不一致。glTF则面向实时渲染,文件小、加载快,默认使用右手坐标系和米制单位,但材质模型以PBR为主,传统Blender材质节点不一定能完整映射。OBJ是最古老的模型格式之一,结构简单,适合几何体交换,但不带场景层级和现代PBR属性,贴图依赖外部MTL文件。批量转换前应该先明确目标平台:进Unity或Unreal通常选FBX;上Web或移动端优先glTF,可选用.glb二进制单文件;OBJ更适合给CAD或仅需要网格的流程。
这些差异并不是说哪种格式更好,而是决定脚本里要做哪些补偿动作。比如Blender导入FBX时可能把单位缩放改变,导出glTF时需要应用缩放或设置导出器系数。OBJ导入后通常需要重算法线,否则在游戏引擎中会看到表面变黑。理解了这些点,后面看到脚本里的对应参数就不会觉得莫名其妙。
用Python脚本遍历目录并批量导出
批量脚本的第一步是拿到文件列表。Python的pathlib或os.walk都可以,这里推荐pathlib.Path.glob,写法更直观。需要支持递归时,使用**通配符。脚本先定义源目录、输出目录和目标格式,然后遍历所有匹配文件。导入前先清空场景,避免上一轮网格残留。导入函数根据文件后缀调用bpy.ops.import_scene.fbx、import_scene.gltf或import_scene.obj。导入完成后,选中所导入的对象,最后调用对应导出操作。
import bpy
import os
from pathlib import Path
SRC = Path("D:/models/in")
DST = Path("D:/models/out")
TARGET_FMT = "GLB" # 可选 FBX OBJ GLB GLTF
DST.mkdir(parents=True, exist_ok=True)
def clear_scene():
bpy.ops.object.select_all(action="SELECT")
bpy.ops.object.delete(use_global=False, confirm=False)
# 清理孤立数据块
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
def import_model(path):
suffix = path.suffix.lower()
if suffix == ".fbx":
bpy.ops.import_scene.fbx(filepath=str(path))
elif suffix == ".glb" or suffix == ".gltf":
bpy.ops.import_scene.gltf(filepath=str(path))
elif suffix == ".obj":
bpy.ops.import_scene.obj(filepath=str(path))
else:
return False
return True
def export_model(path):
if TARGET_FMT == "FBX":
bpy.ops.export_scene.fbx(filepath=str(path), path_mode="COPY", embed_textures=True)
elif TARGET_FMT == "GLB":
bpy.ops.export_scene.gltf(filepath=str(path), export_format="GLB", export_yup=True)
elif TARGET_FMT == "GLTF":
bpy.ops.export_scene.gltf(filepath=str(path), export_format="GLTF_SEPARATE", export_yup=True)
elif TARGET_FMT == "OBJ":
bpy.ops.export_scene.obj(filepath=str(path), use_materials=True, path_mode="COPY")
else:
return False
return True
failed = []
for item in SRC.rglob("*"):
if not item.is_file():
continue
clear_scene()
if not import_model(item):
continue
out_path = DST / (item.stem + (".fbx" if TARGET_FMT == "FBX" else ".glb" if TARGET_FMT == "GLB" else ".gltf" if TARGET_FMT == "GLTF" else ".obj"))
export_model(out_path)
print("转换完成,输出目录:", DST)
这段代码先清空场景,再按后缀导入。注意导出glTF时设置了export_yup=True,是因为glTF规范使用Y轴朝上,而Blender默认Z轴朝上。强制使用Y-up可以让导出结果在大多数Web查看器中朝向正确,代价是脚本内部会进行坐标变换。OBJ导出时启用use_materials会把MTL文件一并写出,方便需要外部材质管理的流程。失败列表预留了failed,但上面代码没有填充,你可以把导出操作放在try-except里,记录具体路径。
还有一点,导入FBX时如果路径包含中文或空格,filepath传入字符串通常没问题,但要确保文件路径是绝对路径或从Path解析后的完整字符串。脚本里的rglob会返回源目录下所有层级文件,遇到同名文件时输出目录可能冲突,建议在输出端也保留相对目录结构。可以用relative_to把相对路径拼接到输出目录,这样大型目录不会覆盖同名模型。
无头模式运行脚本
保存为batch_convert.py后,不需要打开Blender界面。命令行里进入Blender安装目录,Windows通常为C:\Program Files\Blender Foundation\Blender 4.0\blender.exe,macOS通过终端执行/Applications/Blender.app/Contents/MacOS/Blender。命令格式为:blender --background --python batch_convert.py。无头模式不加载界面,启动更快,也不会因为UI上下文限制导致操作失败。Linux下如果Blender在PATH中,直接写blender即可。
blender --background --python batch_convert.py
需要传参数时,可以在命令行后追加--,再从sys.argv读取。Blender会把--后面的内容传入Python脚本,例如:blender --background --python batch_convert.py -- D:/models/in D:/models/out GLB。脚本里用sys.argv获取索引从argv.index('--')+1开始的参数。这样同一份脚本就能处理不同目录和目标格式,不必每次改代码。更推荐的做法是用argparse,但要注意Blender自带的Python环境包含了argparse,可以直接使用。
import sys
args = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
src = Path(args[0]) if len(args) > 0 else Path("D:/models/in")
dst = Path(args[1]) if len(args) > 1 else Path("D:/models/out")
fmt = args[2].upper() if len(args) > 2 else "GLB"
如果你把批量转换放进CI流水线,建议关闭所有渲染相关模块,使用--factory-startup可以避免加载用户配置,进一步减少启动时间。Blender启动参数还包含--python-expr可以直接执行单行表达式,适合非常小的任务。但对于批量转换,独立脚本文件更容易维护和调试。执行时如果遇到segmentation fault,多半是某个导入器对损坏文件处理不佳,建议在import_model里加try-except,逐个捕捉异常后继续下一个文件。
转换质量与性能优化
批量转换不只是能跑通就行,输出质量取决于三个细节:缩放、法线和材质。FBX导入Blender时经常看到默认单位缩放是0.01或100,这跟源软件的厘米、米设置有关。统一做法是在导入后检查对象尺寸,必要时应用缩放:bpy.ops.object.transform_apply(scale=True)。对于glTF导出,也可以设置在导出器中的export_apply=True,这样导出前会把修改器和缩放烘焙到网格。OBJ导入后选择所有网格,使用bpy.ops.mesh.normals_make_consistent(inside=False)重算法线,能解决大部分黑面问题。
# 应用缩放和重算法线
bpy.ops.object.select_all(action="SELECT")
for obj in bpy.context.selected_objects:
if obj.type == "MESH":
bpy.context.view_layer.objects.active = obj
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
bpy.ops.mesh.normals_make_consistent(inside=False)
材质方面,FBX导出的模型如果使用Blender标准BSDF,导入Unity或Unreal后通常能保留基础颜色和金属度,但程序化纹理节点不一定被翻译。glTF则是明确定义了PBR通道,如果模型原本就按Principled BSDF制作,导出效果最稳定。OBJ只有基础颜色贴图,高光和法线贴图不会写入MTL,需另行处理。对这些要求较高的项目,可以在Blender里借助材质烘焙脚本,把复杂节点烘焙到纹理,再导出,但那是另一个话题。
性能上,如果一次转换几千个文件,逐文件启动Blender进程会很慢,所以保持一个进程循环导入导出更快。注意每轮导出前清空场景并释放内存,可以调用bpy.ops.outliner.orphans_purge,但比较耗时。如果确认场景中不会产生孤立数据,也可以跳过清理。对于超大FBX或OBJ,内存占用可能成倍增长,遇到内存不足时,可以将一批分成多个子目录,用多个进程并行转换。Blender无头模式支持多开,但每个进程都要占用数百MB内存,根据机器配置控制并发数。
遇到导入报错或导出为空怎么办
批量脚本最容易在导入损坏文件时中断。常见的报错包括无法读取文件、版本不支持、缺少外部贴图。解决思路是给导入和导出包上try-except,把失败路径写进日志,不让整个批量任务停掉。日志可以是CSV或者纯文本,每一行记录源文件路径、目标格式和异常信息。这样跑完一批后,不用盯屏幕,直接检查失败列表即可。
import logging
logging.basicConfig(filename="convert.log", level=logging.INFO)
try:
if not import_model(item):
continue
export_model(out_path)
logging.info("OK: %s -> %s", item, out_path)
except Exception as e:
logging.error("FAIL: %s -> %s", item, e)
failed.append(str(item))
如果导出的glTF在浏览器中显示全黑,优先检查法线方向和光照。Blender导出的glTF默认会包含法线,但如果原模型法线反了,查看器里就会变暗。可以在Blender中开启法线显示,确认向外。OBJ导出后如果材质不显示,检查MTL文件是否在正确相对路径,或者脚本里的path_mode设置不符合目标工具要求。path_mode='COPY'会把贴图复制到导出目录,path_mode='AUTO'则使用相对路径,跨平台时需要留意路径分隔符。
另外,Blender版本会影响导入器行为。3.x和4.x对FBX导入做了不少改动,如果团队内版本不一致,最好统一为一个稳定版本,避免同样的脚本在不同机器上产生不同结果。可以通过bpy.app.version_string输出当前版本,写进日志方便追踪。

