160 lines
6.8 KiB
Python
160 lines
6.8 KiB
Python
"""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。"""
|
||
reg = registry_rizom_path() if use_registry else None
|
||
cands = candidate_paths(cli_arg, 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
|
||
|
||
|
||
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 次失败(%s),换新实例重试" % (attempt, last))
|
||
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):
|
||
link = mod.CRizomUVLink()
|
||
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})
|
||
# 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:
|
||
_call("Quit", link.Quit, {}, timeout=10)
|
||
except Exception:
|
||
print("[警告] RizomUV Quit 未确认,可能残留 rizomuv 进程占用 license")
|