diff --git a/docs/superpowers/specs/2026-07-21-uv-unwrap-optimization-design.md b/docs/superpowers/specs/2026-07-21-uv-unwrap-optimization-design.md new file mode 100644 index 00000000..e6c7feca --- /dev/null +++ b/docs/superpowers/specs/2026-07-21-uv-unwrap-optimization-design.md @@ -0,0 +1,132 @@ +# UV 展开质量优化 + 线框观察图导出 + +日期:2026-07-21 +范围:`Tools/ModelTranslator/bl_decimate.py`、`Tools/ModelTranslator/model_decimate.py` + +## 背景与动机 + +减面工具当前 UV 流程(`bl_decimate.py`): +- `_unwrap_seam`:锐边(66°)标 seam → `MINIMUM_STRETCH`(SLIM) 展开 → 内置 `pack_islands`。 +- `_do_unwrap`:seam 结果过质量门(翻转 ≤2%、重叠 ≤8%)则用;**一次不过就整个回退 `_unwrap_smart`(Smart UV Project)**。 +- 排布:`_uvpm_repack` 用 UVPackmaster 4 在已有岛上重排,失败保底内置布局。 + +实测 gargoyle(499952→998 面):seam 展开翻转 18.6%、重叠 41.4%,直接退到 smart,最终 **324 个碎岛**、利用率 77.6%。碎岛过多导致烘焙缝多、padding 浪费、纹素利用低。 + +结论:瓶颈在**展开质量**,不在排布。本设计增强展开、并新增 UV 线框观察图便于人工查阅。 + +## 目标 + +1. 展开阶段引入拉伸松弛与纹素密度均衡,降低翻转/拉伸。 +2. seam 失败不再一步退化为碎岛的 smart,而是先松弛、再按更低角度扫描抢救,尽量保住低岛数解析式展开。 +3. 每次减面产出一张 UV 线框观察图,供人工检查切分/排布/碎岛。 + +## 非目标(YAGNI) + +- 不抽独立「多候选择优」模块;改动内联进现有函数。 +- 观察图仅纯 UV 线框,不叠贴图/棋盘格(减面阶段尚无烘焙贴图)。 +- 不改 `model_bake.py` / `model_translator.py` / UVPM 排布逻辑。 +- 不引入 matplotlib/PIL 等新依赖。 + +## 设计 + +### 1. 展开质量增强(minimize_stretch + average_islands_scale) + +`_unwrap_seam`(`bl_decimate.py:128`)在 unwrap 之后、pack 之前插两步: + +```python +bpy.ops.uv.unwrap(method='MINIMUM_STRETCH', margin=PACK_MARGIN) # 已有 +bpy.ops.uv.minimize_stretch(iterations=STRETCH_ITERS) # 新增:角度松弛压翻转/拉伸 +... +bpy.ops.uv.average_islands_scale() # 新增:统一纹素密度 +bpy.ops.uv.pack_islands(rotate=True, margin=PACK_MARGIN) # 已有 +``` + +- 新增常量 `STRETCH_ITERS = 30`。 +- `minimize_stretch` 与现有 unwrap 同为 edit-mode 算子、同上下文,风险低。 +- `average_islands_scale` headless 上下文若不可用:`try/except RuntimeError` 记警告跳过、不中断(与现有 `pack_islands`/UVPM 兜底同风格)。 +- `ANGLE_BASED` 老版本回退分支同样插入这两步(保持一致)。 + +### 2. seam 角度扫描 + 分级抢救 + +**参数化 seam 角度**:`_unwrap_seam` 增加 `seam_angle_deg` 入参(默认沿用 `SEAM_ANGLE_DEG=66.0`),`edges_select_sharp(sharpness=radians(seam_angle_deg))`。 + +**`_do_unwrap`(`bl_decimate.py:267`)改为分级**: + +1. seam@66° → 松弛 → 采集指标 → 测门,过则采用; +2. 不过 → 依次试 `SEAM_ANGLE_SWEEP = (55, 45, 35)`,每档:clear UV → 标 seam@角度 → 展开+松弛 → 采集指标;**在所有过门候选里取 UV 岛数最少者**采用; +3. 全不过门 → `_unwrap_smart` 保底(同现状)。 + +细节: +- **UVPM 重排只在最终选定的 UV 上跑一次**(`_uvpm_repack`),不在每个候选上跑,省时。因此候选比较用的是内置 `pack_islands` 布局下的指标;翻转/重叠是展开固有属性,不随排布器变,用于比较有效。 +- `mode` 统一标注实际生效角度:base 字符串固定为 `seam@<角度>`(首选走通即 `seam@66`),再经 `uvpm_mode_label` 加 `+uvpm` 后缀 → 如 `seam@45+uvpm`。smart 保底路径保持 `smart_fallback`(同现状)。 +- 候选指标采集复用 `_collect_uv_metrics`;每个候选都要跑一次 `_collect_uv_metrics`(含 `_overlap_fraction` 的 edit-mode 算子),已知开销可接受(展开远快于减面)。 +- 代价:一次减面最多 4 轮展开(66/55/45/35)。gargoyle 类正是受益场景。 + +**质量门与常量**:`UV_FLIP_TOL`/`UV_OVERLAP_TOL`/`uv_gate_ok` 不变。新增 `SEAM_ANGLE_SWEEP`。 + +### 3. UV 线框观察图导出 + +最终 UV 定下后(UVPM 重排完成后),导出线框 PNG: + +- 算子:`bpy.ops.uv.export_layout(filepath=..., mode='PNG', size=(UV_PREVIEW_SIZE, UV_PREVIEW_SIZE), opacity=UV_PREVIEW_OPACITY)`。 +- 新增常量 `UV_PREVIEW_SIZE = 1024`、`UV_PREVIEW_OPACITY = 0.25`。 +- 需要正确上下文:对象激活、edit mode、UV 全选。实现时验证 headless 可行性。 +- 输出路径:紧邻 `_low.fbx`,命名 `<名>_low_uv.png`(由 `out_fbx` 派生:同目录、`os.path.splitext(out_fbx)[0] + "_uv.png"`)。 +- 失败:`try/except`(`RuntimeError`/`SystemError`)记警告、不中断,FBX 照常产出。 +- 封装为 `_export_uv_layout(obj, uv_png_path, warnings)`,在 `main()` 导出 FBX 前后择一时机调用(需在切回 OBJECT 模式前后处理好上下文)。 +- `MT_SUMMARY` JSON 增加字段 `uv_preview`(路径或 null)。 + +### 4. CLI 输出(model_decimate.py) + +`model_decimate.py` 打印处(`model_decimate.py:30-35`)追加一行: + +```python +if s.get("uv_preview"): + print(" UV 观察图: %s" % s["uv_preview"]) +``` + +## 数据流 + +``` +FBX → join → triangulate → decimate(±3%, ≤2 retry) + → _do_unwrap: + seam@66 → +stretch → metrics → gate? + ├ pass → 选定 + └ fail → sweep 55/45/35(各 +stretch → metrics)→ 取过门且岛最少 + └ 全失败 → smart_project + → average_islands_scale + pack_islands(在 _unwrap_* 内) + → _uvpm_repack(最终 UV 上一次) + → _export_uv_layout → <名>_low_uv.png + → export_scene.fbx → <名>_low.fbx + → MT_SUMMARY(json){..., uv, uv_preview, warnings} +``` + +## 错误处理 + +| 失败点 | 处理 | +|---|---| +| `minimize_stretch` 不可用 | try/except 记警告,跳过松弛继续 | +| `average_islands_scale` 上下文不可用 | try/except RuntimeError 记警告,跳过 | +| 所有 seam 角度不过门 | 回退 `_unwrap_smart`(保底,同现状) | +| `export_layout` 失败 | try/except 记警告,`uv_preview=null`,不中断 FBX 产出 | +| UVPM 不可用 | 现有逻辑不变(记警告,保留内置布局) | + +原则:**观察图与质量增强均为增强项,任何单点失败都降级而非中断**,FBX 必产出。 + +## 测试与验证 + +1. 现有单测不回归:`python -m unittest tests.test_unity_assets`。 +2. 纯函数可单测:`SEAM_ANGLE_SWEEP` 遍历顺序、候选择优(取过门且岛最少)逻辑抽成纯函数 `pick_best_candidate(candidates)` 便于单测(输入指标 dict 列表,输出选中项)。 +3. 端到端:`python model_decimate.py src/gargoyle.fbx --tris 1000`,对比改动前后指标(岛数/利用率/翻转/重叠)与 `mode` 标注;确认 `src/gargoyle_low_uv.png` 生成且线框可读。 +4. 回归另一模型(如 well1500)确认首选 seam@66 仍走通、未被扫描逻辑破坏。 + +## 验收标准 + +- gargoyle 减面后:走通某档 seam(岛数显著低于 324)**或**明确记录全失败退 smart 的原因;无论哪条路径都产出可读的 `_low_uv.png`。 +- 任一增强算子在 headless 不可用时,流程降级不崩,FBX 与观察图(若 export 本身可用)照常产出。 +- CLI 日志新增 UV 观察图路径行;`mode` 标注实际生效角度。 +- 现有单测全绿。 + +## 未决/实现时验证项 + +- `minimize_stretch` / `average_islands_scale` / `export_layout` 在当前 Blender 5.0 headless 的算子上下文可行性——实现首步用最小脚本各验证一次,不可行则按错误处理表降级并在 spec/README 注明。