"""Export new GLB assets as FBX, scaled to the matching project asset size. The uniform scale is based on the longest bounding-box edge, which preserves each new model's proportions while matching the project's visual scale. """ from pathlib import Path import csv import bpy from mathutils import Vector PROJECT = Path(__file__).resolve().parent.parent ASSET_ROOT = PROJECT / "docs" / "asset" NEW_ROOT = next(path for path in ASSET_ROOT.iterdir() if path.is_dir() and any(path.rglob("*.glb"))) OUTPUT_ROOT = NEW_ROOT.parent / f"{NEW_ROOT.name}_project_scale_fbx" / "models" REFERENCE_DIRS = (PROJECT / "assets" / "Models", PROJECT / "assets" / "Model") SPECIAL_REFERENCES = { "BulletTrail": "BulletTrail", "SM_Item_Bullet_01": "bullet", "SM_Tile_Cube_Water_0": "SM_Tile_Cube_Water_01", "test": "test", "WEP_98k": "Wep_Rifle_98k", "WEP_M4A1 copy": "Wep_Rifle_1", "WEP_Sniper_1": "Wep_Sniper_01", } def clear_scene(): bpy.ops.object.select_all(action="SELECT") bpy.ops.object.delete(use_global=False) def bounds(): points = [] for obj in bpy.context.scene.objects: if obj.type == "MESH": points.extend(obj.matrix_world @ Vector(corner) for corner in obj.bound_box) if not points: raise RuntimeError("model has no mesh objects") low = [min(point[i] for point in points) for i in range(3)] high = [max(point[i] for point in points) for i in range(3)] return tuple(high[i] - low[i] for i in range(3)) def load(path: Path): clear_scene() if path.suffix.lower() == ".glb": bpy.ops.import_scene.gltf(filepath=str(path)) else: bpy.ops.import_scene.fbx(filepath=str(path)) # Cocos Creator's FBX-to-glTF converter writes invalid accessors for # mesh objects that have materials but contain no faces. Blender keeps # those empty mesh objects when importing some GLB files, so remove them. for obj in list(bpy.context.scene.objects): if obj.type == "MESH" and len(obj.data.polygons) == 0: bpy.data.objects.remove(obj, do_unlink=True) def normalized(name: str) -> str: return "".join(char.lower() for char in name if char.isalnum()) references = {} for folder in REFERENCE_DIRS: for file in folder.rglob("*.fbx"): references[normalized(file.stem)] = file OUTPUT_ROOT.mkdir(parents=True, exist_ok=True) report = [] errors = [] for glb in sorted(NEW_ROOT.rglob("*.glb")): reference_name = SPECIAL_REFERENCES.get(glb.stem, glb.stem) reference = references.get(normalized(reference_name)) try: if reference is None: raise RuntimeError(f"no project reference found for {reference_name}") load(reference) reference_size = bounds() target_length = max(reference_size) load(glb) source_size = bounds() source_length = max(source_size) if source_length == 0: raise RuntimeError("model bounding box has zero size") scale = target_length / source_length for obj in bpy.context.scene.objects: if obj.parent is None: obj.location *= scale obj.scale *= scale output = OUTPUT_ROOT / glb.relative_to(NEW_ROOT).with_suffix(".fbx") output.parent.mkdir(parents=True, exist_ok=True) bpy.ops.export_scene.fbx( filepath=str(output), 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, ) report.append((str(glb.relative_to(NEW_ROOT)), reference.name, scale, source_length, target_length)) print(f"OK {glb.name}: x{scale:.8g} -> {reference.name}") except Exception as error: errors.append((str(glb), str(error))) print(f"FAIL {glb.name}: {error}") with (OUTPUT_ROOT / "scale_report.csv").open("w", newline="", encoding="utf-8-sig") as file: writer = csv.writer(file) writer.writerow(("source", "reference", "uniform_scale", "source_longest_edge", "target_longest_edge")) writer.writerows(report) if errors: with (OUTPUT_ROOT / "errors.csv").open("w", newline="", encoding="utf-8-sig") as file: writer = csv.writer(file) writer.writerow(("source", "error")) writer.writerows(errors) print(f"Finished: {len(report)} scaled, {len(errors)} failed. Output: {OUTPUT_ROOT}") raise SystemExit(1 if errors else 0)