Files
AIC-Project/Tools/ModelTranslator/rizom_unwrap.py
T

178 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""RizomUV 自动展开:官方 RizomUVLink 驱动 rizomuv.exe 做 AutoSeam/Unfold/Pack。
仅在系统 Python 中使用(不进 Blender);任何不可用/失败抛 RizomError
由 model_decimate.py 回退 Blender seam 展开。"""
import os
import sys
import threading
DEFAULT_RIZOM = r"C:\Program Files\RizomUV2025\rizomuv.exe"
MAP_RES = 2048 # Pack 目标贴图分辨率
PADDING_PX = 4 # 岛间距(像素)= bl_decimate.PACK_MARGIN(0.002) * 2048
TIMEOUT_S = 300 # 单条命令超时
class RizomError(Exception):
"""RizomUV 不可用或处理失败(上层据此回退)。"""
def candidate_paths(cli_arg=None, env_val=None, reg_val=None, default=DEFAULT_RIZOM):
"""rizomuv.exe 自动发现候选:环境变量 > 注册表 > 默认安装位置(显式 CLI 路径走独占分支不进此链)。"""
return [p for p in (cli_arg, env_val, reg_val, default) if p]
def registry_rizom_path():
"""官方 README 的注册表查找(HKLM\\SOFTWARE\\Rizom Lab\\RizomUV VS RS 202X.Y),
新版本优先;找不到或非 Windows 返回 None。"""
try:
import winreg
except ImportError:
return None
for i in range(9, 1, -1):
for j in range(10, -1, -1):
if i == 2 and j < 2:
continue
key_path = "SOFTWARE\\Rizom Lab\\RizomUV VS RS 202%d.%d" % (i, j)
try:
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path) as key:
val = winreg.QueryValue(key, "rizomuv.exe")
# 注册表值是不带后缀的安装前缀(官方只取 dirname 用),拼回真实 exe
return os.path.join(os.path.dirname(val), "rizomuv.exe")
except OSError:
continue
return None
def find_rizomuv(cli_arg=None, use_registry=True, default=DEFAULT_RIZOM):
"""返回第一个真实存在的 rizomuv.exe;都不存在抛 RizomError。
显式指定 cli_arg 时独占生效:路径无效直接报错,不回落其它候选。"""
if cli_arg:
if os.path.isfile(cli_arg):
return cli_arg
raise RizomError("指定的 rizomuv.exe 不存在:%s" % cli_arg)
reg = registry_rizom_path() if use_registry else None
cands = candidate_paths(None, os.environ.get("RIZOMUV_EXE"), reg, default)
for p in cands:
if os.path.isfile(p):
return p
raise RizomError("找不到 rizomuv.exe(尝试过:%s" % "; ".join(cands))
def _import_link(exe):
"""从 RizomUV 安装目录动态引入官方 RizomUVLink 模块(随安装自带,MIT)。
同进程只支持单一安装路径(import 缓存,首次 import 后不会再切换目录)。"""
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 _check_pack_result(res):
"""Pack 返回 Coverages 空列表 = 没有任何平面岛可打包——Unfold 未生效。
实测这是 RizomUV 无有效授权时的静默失败形态(命令全部"成功"但解算不执行)。"""
if isinstance(res, dict) and "Coverages" in res and not res["Coverages"]:
raise RizomError("Pack 无任何平面岛(Unfold 未生效)——请检查 RizomUV 授权是否已激活")
RETRY = 1 # 实例级重试次数:RizomUV IPC 有 2000ms 内部响应窗,偶发 not responding
def rizom_unwrap(fbx_path, exe, map_res=MAP_RES, padding_px=PADDING_PX):
"""AutoSeam 全自动切缝 -> Unfold -> Pack -> 覆盖保存 fbx_path。失败抛 RizomError。
间歇性 IPC 失败自动换新实例重试一次。"""
mod = _import_link(exe)
fbx_abs = os.path.abspath(fbx_path)
pad_uv = float(padding_px) / map_res
last = None
for attempt in range(1 + RETRY):
if attempt:
print("[警告] RizomUV 第 %d 次失败,换新实例重试" % attempt)
try:
return _unwrap_once(mod, exe, fbx_abs, map_res, pad_uv)
except RizomError as e:
last = e
raise RizomError("重试后仍失败:%s" % last)
def _unwrap_once(mod, exe, fbx_abs, map_res, pad_uv):
try:
link = mod.CRizomUVLink()
except Exception as e: # 构造失败也归一为 RizomError,维持"失败只抛 RizomError"契约
raise RizomError("RizomUVLink 实例创建失败:%s" % e)
try:
_call("RunRizomUV", lambda _p: link.RunRizomUV(exePath=exe), None, timeout=60)
except RizomError as e:
raise RizomError("RizomUV 启动失败(license/环境):%s" % e)
try:
_call("Load", link.Load, {"File.Path": fbx_abs, "File.XYZ": True})
# AutoSeamMOSAIC(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"})
r = _call("Unfold", link.Unfold, {"PrimType": "Edge", "WorkingSet": "Visible",
"BorderIntersections": True,
"TriangleFlips": True})
if isinstance(r, dict) and r.get("BijectionFailedIslandIDs"):
print("[警告] Unfold 有 %d 个岛双射失败" % len(r["BijectionFailedIslandIDs"]))
# Scaling.Mode=3 (AVG_RATIO_OF_GROUP):官方文档建议首次展开后用该模式统一密度。
# 注:pyd docstring 写的 Global 包装层已被 2025 应用侧拒绝(exe 内 Pack schema
# 无 Global,打包属性直接为顶层参数),故扁平传参。
r = _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}})
_check_pack_result(r)
_call("Save", link.Save, {"File.Path": fbx_abs})
finally:
try:
_call("Quit", link.Quit, {}, timeout=10)
except Exception:
print("[警告] RizomUV Quit 未确认,可能残留 rizomuv 进程占用 license")