"""ModelTranslator CLI:FBX -> 纯模型 FBX + XPbr 贴图 + Unity .mat/.meta。 用法:python model_translator.py src/tong.fbx [-o out] [--max-size 2048] [--blender exe] """ import argparse import json import os import sys import unity_assets as ua from mt_run import HERE, find_blender, run_blender_script from png_compress import compress_dir_pngs def write_unity_assets(outdir, summary, max_size): remaps = [] for m in summary["materials"]: base_png = os.path.join(outdir, m["base_png"]) mix_png = os.path.join(outdir, m["mix_png"]) base_guid = ua.guid_for(base_png) mix_guid = ua.guid_for(mix_png) alpha_test = m["alpha_mode"] == "transparency" with open(base_png + ".meta", "w", encoding="utf-8", newline="\n") as f: f.write( ua.texture_meta( base_guid, srgb=True, max_size=max_size, alpha_is_transparency=alpha_test, ) ) with open(mix_png + ".meta", "w", encoding="utf-8", newline="\n") as f: f.write(ua.texture_meta(mix_guid, srgb=False, max_size=max_size)) mat_name = ( summary["model"] if m["safe_name"] == summary["model"] else "%s_%s" % (summary["model"], m["safe_name"]) ) mat_path = os.path.join(outdir, mat_name + ".mat") mat_guid = ua.guid_for(mat_path) with open(mat_path, "w", encoding="utf-8", newline="\n") as f: f.write( ua.material_yaml( mat_name, ua.XPBR_SHADER_GUID, base_guid, mix_guid, alpha_test ) ) with open(mat_path + ".meta", "w", encoding="utf-8", newline="\n") as f: f.write(ua.material_meta(mat_guid)) remaps.append((m["name"], mat_guid)) fbx_path = os.path.join(outdir, summary["fbx"]) fbx_guid = ua.guid_for(fbx_path) with open(fbx_path + ".meta", "w", encoding="utf-8", newline="\n") as f: f.write(ua.model_meta(fbx_guid, remaps)) return { "fbx": fbx_path, "fbx_guid": fbx_guid, "materials": remaps, "outdir": outdir, } def report(summary, outdir): print("== %s -> %s" % (summary["model"], outdir)) for m in summary["materials"]: print( " 材质 %-24s A通道=%s 尺寸=%dx%d" % (m["name"], m["alpha_mode"], m["size"][0], m["size"][1]) ) for k, v in m["sources"].items(): print(" %-10s %s" % (k, v or "(常量)")) for w in m["warnings"]: print(" [警告] %s" % w) def translate_fbx(src, outdir, max_size=2048, blender=None): """Convert bake-output FBX (+ sibling PBR maps) into XPbr assets. Returns summary dict.""" blender_exe = find_blender(blender) os.makedirs(outdir, exist_ok=True) summary = run_blender_script( blender_exe, "bl_convert.py", [src, outdir, str(max_size)] ) compress_dir_pngs(outdir, suffixes=("_base.png", "_mix.png")) unity = write_unity_assets(outdir, summary, max_size) return {**summary, "unity": unity} def main(): ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("input", help="FBX 文件或目录") ap.add_argument("-o", "--out", default=os.path.join(HERE, "out_xpbr")) ap.add_argument("--max-size", type=int, default=2048) ap.add_argument("--blender", default=None) args = ap.parse_args() if os.path.isdir(args.input): srcs = [ os.path.join(args.input, f) for f in sorted(os.listdir(args.input)) if f.lower().endswith(".fbx") ] if not srcs: sys.exit("目录中没有 FBX:%s" % args.input) else: srcs = [args.input] for src in srcs: name = os.path.splitext(os.path.basename(src))[0] outdir = os.path.join(args.out, name) summary = translate_fbx(src, outdir, args.max_size, args.blender) report(summary, outdir) if __name__ == "__main__": main()