"""Batch-convert GLB files to FBX through Blender. Run this file with Blender's Python, not the system Python. The script keeps the source folder layout and writes an errors.csv next to the output files. """ from __future__ import annotations import argparse import csv import sys from pathlib import Path import bpy def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("--source", required=True, type=Path) parser.add_argument("--destination", required=True, type=Path) parser.add_argument("--overwrite", action="store_true") return parser.parse_args(sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else []) def clear_scene() -> None: bpy.ops.object.select_all(action="SELECT") bpy.ops.object.delete(use_global=False) for collection in (bpy.data.meshes, bpy.data.materials, bpy.data.images, bpy.data.armatures): for item in list(collection): collection.remove(item) def main() -> int: args = parse_args() source = args.source.resolve() destination = args.destination.resolve() if not source.is_dir(): print(f"ERROR: source folder does not exist: {source}") return 2 if source == destination: print("ERROR: destination must be different from source") return 2 glb_files = sorted(source.rglob("*.glb")) destination.mkdir(parents=True, exist_ok=True) errors: list[tuple[str, str]] = [] converted = skipped = 0 for index, input_file in enumerate(glb_files, start=1): output_file = destination / input_file.relative_to(source).with_suffix(".fbx") if output_file.exists() and not args.overwrite: print(f"[{index}/{len(glb_files)}] SKIP {input_file.name}") skipped += 1 continue output_file.parent.mkdir(parents=True, exist_ok=True) try: clear_scene() bpy.ops.import_scene.gltf(filepath=str(input_file)) bpy.ops.export_scene.fbx( filepath=str(output_file), use_selection=False, object_types={"EMPTY", "CAMERA", "LIGHT", "ARMATURE", "MESH", "OTHER"}, use_mesh_modifiers=True, add_leaf_bones=False, path_mode="COPY", embed_textures=True, ) converted += 1 print(f"[{index}/{len(glb_files)}] OK {input_file.name}") except Exception as exc: # continue converting the remaining assets errors.append((str(input_file), repr(exc))) print(f"[{index}/{len(glb_files)}] FAIL {input_file.name}: {exc}") if errors: error_log = destination / "errors.csv" with error_log.open("w", newline="", encoding="utf-8-sig") as handle: writer = csv.writer(handle) writer.writerow(("source_file", "error")) writer.writerows(errors) print(f"Error log: {error_log}") print(f"Finished: {converted} converted, {skipped} skipped, {len(errors)} failed.") return 1 if errors else 0 if __name__ == "__main__": raise SystemExit(main())