ModelTranslator: CLI runner with Unity asset generation
This commit is contained in:
@@ -0,0 +1,106 @@
|
|||||||
|
"""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 shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import unity_assets as ua
|
||||||
|
|
||||||
|
DEFAULT_BLENDER = r"D:\tools\blender-5.0.0-windows-x64\blender.exe"
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
def find_blender(cli_arg):
|
||||||
|
for cand in (cli_arg, os.environ.get("BLENDER_EXE"), DEFAULT_BLENDER,
|
||||||
|
shutil.which("blender")):
|
||||||
|
if cand and os.path.isfile(cand):
|
||||||
|
return cand
|
||||||
|
sys.exit("找不到 blender.exe,请用 --blender 指定或设置环境变量 BLENDER_EXE")
|
||||||
|
|
||||||
|
|
||||||
|
def run_blender(blender, src, outdir, max_size):
|
||||||
|
cmd = [blender, "-b", "--factory-startup",
|
||||||
|
"--python", os.path.join(HERE, "bl_convert.py"),
|
||||||
|
"--", src, outdir, str(max_size)]
|
||||||
|
proc = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8",
|
||||||
|
errors="replace")
|
||||||
|
summary = None
|
||||||
|
for line in (proc.stdout or "").splitlines():
|
||||||
|
if line.startswith("MT_SUMMARY "):
|
||||||
|
summary = json.loads(line[len("MT_SUMMARY "):])
|
||||||
|
if proc.returncode != 0 or summary is None:
|
||||||
|
sys.stderr.write(proc.stdout[-2000:] + "\n" + proc.stderr[-2000:] + "\n")
|
||||||
|
sys.exit("Blender 转换失败:%s" % src)
|
||||||
|
return summary
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
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))
|
||||||
|
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 = "%s_%s" % (summary["model"], m["safe_name"])
|
||||||
|
mat_path = os.path.join(outdir, mat_name + ".mat")
|
||||||
|
mat_guid = ua.guid_for(mat_path)
|
||||||
|
alpha_test = m["alpha_mode"] == "transparency"
|
||||||
|
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) # 必须在 open("w") 截断文件之前读取,否则复用失效
|
||||||
|
with open(fbx_path + ".meta", "w", encoding="utf-8", newline="\n") as f:
|
||||||
|
f.write(ua.model_meta(fbx_guid, remaps))
|
||||||
|
|
||||||
|
|
||||||
|
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 main():
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
|
ap.add_argument("input", help="FBX 文件或目录")
|
||||||
|
ap.add_argument("-o", "--out", default=os.path.join(HERE, "out"))
|
||||||
|
ap.add_argument("--max-size", type=int, default=2048)
|
||||||
|
ap.add_argument("--blender", default=None)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
blender = find_blender(args.blender)
|
||||||
|
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 = run_blender(blender, src, outdir, args.max_size)
|
||||||
|
write_unity_assets(outdir, summary, args.max_size)
|
||||||
|
report(summary, outdir)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user