diff --git a/Tools/ModelTranslator/rizom_unwrap.py b/Tools/ModelTranslator/rizom_unwrap.py index d3a2b3bb..d4e32eb6 100644 --- a/Tools/ModelTranslator/rizom_unwrap.py +++ b/Tools/ModelTranslator/rizom_unwrap.py @@ -50,3 +50,93 @@ def find_rizomuv(cli_arg=None, use_registry=True, default=DEFAULT_RIZOM): if os.path.isfile(p): return p raise RizomError("找不到 rizomuv.exe(尝试过:%s)" % "; ".join(cands)) + + +def _import_link(exe): + """从 RizomUV 安装目录动态引入官方 RizomUVLink 模块(随安装自带,MIT)。""" + link_dir = os.path.join(os.path.dirname(exe), "RizomUVLink") + if not os.path.isdir(link_dir): + raise RizomError("RizomUVLink 目录不存在:%s" % link_dir) + if link_dir not in sys.path: + sys.path.insert(0, link_dir) + try: + import RizomUVLink + return RizomUVLink + except Exception as e: # pyd 与 Python 版本不匹配等 + raise RizomError("RizomUVLink 导入失败:%s" % e) + + +def _call(label, fn, params, timeout=TIMEOUT_S): + """带超时执行一条 RizomUV 命令;超时或 CZEx 抛 RizomError。""" + out, err = [], [] + + def run(): + try: + out.append(fn(params)) + except Exception as e: + err.append(e) + + t = threading.Thread(target=run, daemon=True) + t.start() + t.join(timeout) + if t.is_alive(): + raise RizomError("%s 超时(%ds)" % (label, timeout)) + if err: + raise RizomError("%s 失败:%s" % (label, err[0])) + res = out[0] if out else None + # 部分命令(如 Load 文件不存在)不抛 CZEx,而是返回 {'Error': {'Msg':..,'Code':..}} + if isinstance(res, dict) and res.get("Error"): + raise RizomError("%s 失败:%s" % (label, res["Error"])) + return res + + +def rizom_unwrap(fbx_path, exe, map_res=MAP_RES, padding_px=PADDING_PX): + """AutoSeam 全自动切缝 -> Unfold -> Pack -> 覆盖保存 fbx_path。失败抛 RizomError。""" + mod = _import_link(exe) + link = mod.CRizomUVLink() + try: + link.RunRizomUV(exePath=exe) + except Exception as e: + raise RizomError("RizomUV 启动失败(license/环境):%s" % e) + fbx_abs = os.path.abspath(fbx_path) + pad_uv = float(padding_px) / map_res + try: + _call("Load", link.Load, {"File.Path": fbx_abs, "File.XYZ": True}) + # AutoSeam:MOSAIC(QuasiDevelopable) + 切柄/连洞 + 防重叠后处理 + _call("Select(AutoSeam)", link.Select, { + "PrimType": "Edge", "WorkingSet": "Visible", + "Select": True, "ResetBefore": True, + "Auto": { + "QuasiDevelopable": {"Developability": 0.5, "IslandPolyNBMin": 1, + "AreaMinRatio": 0.0, "FitCones": False, + "Straighten": True}, + "HandleCutter": True, + "PipesCutter": True, + "SkeletonUnoverlap": {}, + "FlatteningMode": 0, + "FlatteningUnfoldParams": {"BorderIntersections": True, + "TriangleFlips": True}, + "StoreCoordsUVW": True, + "Quality": 0.5, + }}) + _call("Cut", link.Cut, {"PrimType": "Edge", "WorkingSet": "Visible"}) + _call("Unfold", link.Unfold, {"PrimType": "Edge", "WorkingSet": "Visible", + "BorderIntersections": True, + "TriangleFlips": True}) + # Scaling.Mode=3 (AVG_RATIO_OF_GROUP):官方文档建议首次展开后用该模式统一密度。 + # 注:pyd docstring 写的 Global 包装层已被 2025 应用侧拒绝(exe 内 Pack schema + # 无 Global,打包属性直接为顶层参数),故扁平传参。 + _call("Pack", link.Pack, { + "RootGroup": "RootGroup", "WorkingSet": "Visible", + "ProcessTileSelection": False, "RecursionDepth": 1, + "Translate": True, + "MapResolution": map_res, + "PaddingSize": pad_uv, "MarginSize": pad_uv, + "Rotate": {"Step": 90.0}, + "Scaling": {"Mode": 3}}) + _call("Save", link.Save, {"File.Path": fbx_abs}) + finally: + try: + link.Quit({}) + except Exception: + pass