diff --git a/Tools/ModelTranslator/bl_decimate.py b/Tools/ModelTranslator/bl_decimate.py index 34c7f63f..58274f89 100644 --- a/Tools/ModelTranslator/bl_decimate.py +++ b/Tools/ModelTranslator/bl_decimate.py @@ -103,6 +103,9 @@ def _decimate(obj, ratio): SEAM_ANGLE_DEG = 66.0 # 锐边阈值:两面夹角超此值标 seam 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): @@ -204,21 +207,81 @@ def _overlap_fraction(obj): 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): - """按模式展开;seam 质量不达标自动回退 smart。返回 uv 指标 dict(含 mode)。""" + """按模式展开(排布默认 UVPM4 增强,失败保底内置 pack); + seam 质量不达标自动回退 smart。返回 uv 指标 dict(含 mode)。""" if mode == "seam": _unwrap_seam(obj, warnings) + uvpm = _uvpm_repack(obj, warnings) m = _collect_uv_metrics(obj) if uv_gate_ok(m["flipped"], m["overlap"]): - m["mode"] = "seam" + m["mode"] = uvpm_mode_label("seam", uvpm) return m warnings.append("seam 展开质量不达标(翻转 %.1f%% 重叠 %s),回退 smart_project" % (m["flipped"] * 100, "%.1f%%" % (m["overlap"] * 100) if m["overlap"] is not None else "未知")) mode = "smart_fallback" _unwrap_smart(obj) + uvpm = _uvpm_repack(obj, warnings) 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