Files
AIC-Project/docs/superpowers/specs/2026-07-21-uv-island-reduction-design.md
T

6.6 KiB
Raw Blame History

UV 减岛优化:--max-overlap 参数 + 减面前网格清理

日期:2026-07-21 范围:Tools/ModelTranslator/bl_decimate.pyTools/ModelTranslator/model_decimate.py

背景与动机

减面工具的 seam 角度扫描(66→55→45→35°)在质量门(翻转 ≤2%、重叠 ≤8%)失败时下探角度抢救。角度越低 → seam 越多 → UV 岛越碎。实测 gargoyle 3000 面走到 seam@35,产生 686 个碎岛、利用率仅 38.2%

关键事实:

  • UVPackmaster 只排布、不合并岛,无法减少岛数。
  • 低利用率是岛数过多的症状——每岛一圈 padding,岛越碎、总周长越大、留白越多。
  • 两个问题同源同解:减少 seam/岛数(展开阶段),利用率随之回升。

用户诉求:岛数优先,愿意用少量重叠/利用率换更少的岛。

目标

  1. 让更高的 seam 角度(岛更少)能通过质量门被选中——通过可运行时覆盖的重叠容忍(新增 --max-overlap)。翻转门限保持严格。
  2. 减面前清理源网格(焊接重合点 + 去退化面),给 collapse 更干净的流形,从源头减少被迫加的 seam。
  3. 默认行为不变;减岛为显式开启。

非目标(YAGNI

  • 不改 UVPM 排布逻辑(它减不了岛)。
  • 不放宽翻转门限 UV_FLIP_TOL(翻转无法靠排布修复)。
  • 不改减面后低模(清理只在减面前的高模上做,避免改变目标面数)。
  • 不改 model_bake.py / model_translator.py / uv_preview.py
  • 不做减面后的碎岛后处理/岛合并(无通用可靠算子)。

设计

1. --max-overlap 参数穿透

重叠容忍从模块常量升级为可运行时覆盖,翻转门限不动。

bl_decimate.py

  • uv_gate_ok(flipped, overlap, overlap_tol=UV_OVERLAP_TOL)return flipped <= UV_FLIP_TOL and (overlap or 0.0) <= overlap_tol。加默认参数,现有调用与测试(uv_gate_ok(0.0, 0.0) 等)不受影响。
  • pick_best_candidate(candidates, overlap_tol=UV_OVERLAP_TOL):把 overlap_tol 透传给 uv_gate_ok
  • _do_unwrap(obj, mode, warnings, overlap_tol=UV_OVERLAP_TOL):扫描循环内 uv_gate_ok(m["flipped"], m["overlap"], overlap_tol)pick_best_candidate(candidates, overlap_tol) 均用它。
  • main():argv 增加第 5 个可选位置参数 max_overlap;解析 overlap_tol = float(argv[4]) if len(argv) > 4 and argv[4] else UV_OVERLAP_TOL;传入 _do_unwrap

model_decimate.py

  • 新增 --max-overlaptype=float,默认 None)。
  • 传参:run_blender_script(..., [args.input, out_fbx, str(args.tris), args.unwrap, "" if args.max_overlap is None else str(args.max_overlap)])(第 5 位;None → 空串,Blender 侧回落到内置默认)。
  • 校验:--max-overlap 若给定需在 (0, 1] 内,否则 ap.error

日志与 mode 标注不变。放宽后,若 seam@55 重叠 12% ≤ 15%,即被选中,岛数远少于 seam@35

2. 减面前网格清理

bl_decimate.py 新增 _clean_mesh(obj, warnings),在 main()obj = join_meshes(meshes) 之后、_triangulate(obj) 之前调用:

- 进入 EDIT、全选
- remove_doubles(threshold=weld)   # 焊接重合点
- dissolve_degenerate()            # 去零面积/退化面
- 回 OBJECT
  • 焊接阈值相对局部包围盒对角线:新常量 REL_WELD = 1e-4(对角线的 0.01%)。用 obj.bound_box(局部坐标 8 角)算对角线 diagweld = diag * REL_WELD。避免 cm/m 单位差异导致绝对阈值焊过头或焊不动。diag <= 0(退化输入)时跳过焊接、记警告。
  • 两步各自 try/exceptRuntimeError):失败记警告、跳过、不中断。
  • 清理在三角化前做(remove_doubles/dissolve_degenerate 对任意网格有效;更干净的流形让后续三角化+collapse 少产狭长三角)。

3. 数据流

FBX → join → _clean_mesh(焊接+去退化) → triangulate → decimate(±3%)
    → _do_unwrap(overlap_tol=max_overlap or 8%):
         seam 66→55→45→35(各 unwrap+stretch+均衡,用 overlap_tol 判门)
           首个过门即选(角度高→岛少);pick_best_candidate(overlap_tol) 兜底择优
           全失败 → smart
    → UVPM 一次 → 抽 UV 几何 sidecar → export fbx
    → MT_SUMMARY(json)

错误处理

失败点 处理
remove_doubles / dissolve_degenerate 不可用 try/except RuntimeError 记警告,跳过该步
包围盒对角线 ≤ 0 跳过焊接,记警告
--max-overlap 越界 (0,1] model_decimateap.error 提前拒绝
max_overlap 空/缺省 Blender 侧回落 UV_OVERLAP_TOL(默认 8%),行为不变

原则:清理与门限放宽均为可选增强,单点失败降级不中断,FBX 必产出。

测试与验证

  1. 纯函数 TDDtests/test_bl_decimate.py):
    • uv_gate_okoverlap_tol:默认值下同现状;显式 overlap_tol=0.15 时 overlap=0.12 通过、0.16 不过;翻转仍按 UV_FLIP_TOL 严格(overlap_tol 放宽不影响翻转判定)。
    • pick_best_candidateoverlap_tol:放宽后原本不过门(overlap 介于 8%~15%)的候选变为过门并可被选中;无参时同现状。
  2. 现有单测不回归python -m unittest tests.test_bl_decimate tests.test_uv_preview tests.test_unity_assets
  3. 端到端
    • python model_decimate.py src/gargoyle.fbx --tris 3000 --max-overlap 0.15:对比基线(686 岛 / 38.2% / seam@35 / 重叠 6.3%)——期望走更高角度、岛数明显下降、利用率上升;产出 FBX + UV 观察图。
    • python model_decimate.py src/gargoyle.fbx --tris 3000(无 --max-overlap):确认默认行为不回归(仍 8% 门限)。注意网格清理默认开启,默认跑的指标可能与历史基线略有不同——如变化记录并说明。

验收标准

  • --max-overlap 0.15 下 gargoyle 3000UV 岛数显著低于 686 且利用率高于 38.2%;mode 反映实际生效的更高角度(如 seam@55/seam@45)。
  • 默认路径(无 --max-overlap):翻转门限严格不变;流程不崩,FBX + 观察图照常产出。
  • 网格清理任一算子不可用时降级不中断。
  • 现有单测全绿;新增纯函数测试通过。

实现注意

  • _do_unwrap/pick_best_candidate/uv_gate_ok 的新参数一律 keyword-default,保持向后兼容与现有测试不改。
  • argv 第 5 位为空串时必须回落默认,不能 float("") 崩溃。
  • _clean_mesh 改变面数(去退化会略减面),但发生在 decimate 之前,decimate 的 ±3% 修正逻辑照常把面数拉到目标,无影响。