ModelTranslator: 展开路径接入 UVPM4 排布——保底+增强,失败回退内置 pack

This commit is contained in:
ud18010
2026-07-20 12:26:28 +08:00
parent 9fe93f3c92
commit d5b777b6b7
+66 -3
View File
@@ -103,6 +103,9 @@ def _decimate(obj, ratio):
SEAM_ANGLE_DEG = 66.0 # 锐边阈值:两面夹角超此值标 seam SEAM_ANGLE_DEG = 66.0 # 锐边阈值:两面夹角超此值标 seam
PACK_MARGIN = 0.002 # 岛间距 ≈ 2048 图 4px PACK_MARGIN = 0.002 # 岛间距 ≈ 2048 图 4px
UVPM_EXT = "bl_ext.user_default.uvpackmaster4" # UVPackmaster 4 扩展模块名
UVPM_PIXEL_MARGIN = 4 # UVPM 岛间距(像素),与 PACK_MARGIN * UVPM_TEX_SIZE 同口径
UVPM_TEX_SIZE = 2048
def _clear_uv_layers(mesh): def _clear_uv_layers(mesh):
@@ -204,21 +207,81 @@ def _overlap_fraction(obj):
return round(sum(1 for p in polys if p.select) / float(len(polys)), 4) return round(sum(1 for p in polys if p.select) / float(len(polys)), 4)
_uvpm_failed = False # 进程内失败一次即不再重试(避免重复警告与启动开销)
def _uvpm_enable():
"""启用 UVPM4 扩展(幂等)。headless 下先补丁 GPU shader 创建——
UVPM4 导入期为视口覆盖层建 shader,后台模式无 GPU 绘图会 SystemError
覆盖层仅交互用,pack 不受影响。引擎路径经注册表自动发现。"""
import bpy
import addon_utils
if bpy.app.background:
import gpu
orig = gpu.shader.from_builtin
if not getattr(orig, "_mt_safe", False):
def _safe(*a, **k):
try:
return orig(*a, **k)
except SystemError:
return None
_safe._mt_safe = True
gpu.shader.from_builtin = _safe
# default_set=True 才建 preferences.addons 条目(UVPM4 register 依赖);
# factory-startup 关闭偏好自动保存,不会写盘
if addon_utils.enable(UVPM_EXT, default_set=True) is None:
raise RuntimeError("扩展 %s 启用失败(未安装或版本不兼容)" % UVPM_EXT)
def _uvpm_repack(obj, warnings):
"""UVPM4 重排当前 UV 布局,成功返回 True;任何失败记警告返回 False,
保底布局(pack_islands/smart_project)原样保留。"""
global _uvpm_failed
if _uvpm_failed:
return False
import bpy
try:
_uvpm_enable()
p = bpy.context.scene.uvpm4_props.default_main_props
p.pixel_margin_enable = True
p.pixel_margin = UVPM_PIXEL_MARGIN
p.pixel_margin_tex_size = UVPM_TEX_SIZE
bpy.context.scene.tool_settings.use_uv_select_sync = True
bpy.context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
try:
ret = bpy.ops.uvpackmaster4.pack(mode_id='pack.single_tile',
pack_op_type='0')
finally:
bpy.ops.object.mode_set(mode='OBJECT')
if 'FINISHED' not in ret:
raise RuntimeError("pack 返回 %s" % sorted(ret))
return True
except Exception as e:
_uvpm_failed = True
warnings.append("UVPackmaster 排布不可用(%s),沿用内置 pack 布局" % e)
return False
def _do_unwrap(obj, mode, warnings): def _do_unwrap(obj, mode, warnings):
"""按模式展开;seam 质量不达标自动回退 smart。返回 uv 指标 dict(含 mode)。""" """按模式展开(排布默认 UVPM4 增强,失败保底内置 pack);
seam 质量不达标自动回退 smart。返回 uv 指标 dict(含 mode)。"""
if mode == "seam": if mode == "seam":
_unwrap_seam(obj, warnings) _unwrap_seam(obj, warnings)
uvpm = _uvpm_repack(obj, warnings)
m = _collect_uv_metrics(obj) m = _collect_uv_metrics(obj)
if uv_gate_ok(m["flipped"], m["overlap"]): if uv_gate_ok(m["flipped"], m["overlap"]):
m["mode"] = "seam" m["mode"] = uvpm_mode_label("seam", uvpm)
return m return m
warnings.append("seam 展开质量不达标(翻转 %.1f%% 重叠 %s),回退 smart_project" warnings.append("seam 展开质量不达标(翻转 %.1f%% 重叠 %s),回退 smart_project"
% (m["flipped"] * 100, % (m["flipped"] * 100,
"%.1f%%" % (m["overlap"] * 100) if m["overlap"] is not None else "未知")) "%.1f%%" % (m["overlap"] * 100) if m["overlap"] is not None else "未知"))
mode = "smart_fallback" mode = "smart_fallback"
_unwrap_smart(obj) _unwrap_smart(obj)
uvpm = _uvpm_repack(obj, warnings)
m = _collect_uv_metrics(obj) m = _collect_uv_metrics(obj)
m["mode"] = mode if mode == "smart_fallback" else "smart" m["mode"] = uvpm_mode_label(mode if mode == "smart_fallback" else "smart", uvpm)
return m return m