Files
Alien_Sniper/tools/scale_new_models_to_project.py
T
2026-07-21 18:34:32 +08:00

118 lines
4.4 KiB
Python

"""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)