Files
AIC-Project/docs/superpowers/specs/2026-07-21-uv-unwrap-optimization-design.md
T
ud18010andClaude Opus 4.8 e8c0804582 ModelTranslator: UV 展开质量优化设计文档
减面 UV 流程增强:minimize_stretch 松弛 + average_islands_scale
纹素均衡;seam 失败改为角度扫描分级抢救(66/55/45/35)取过门且
岛最少者,全失败才退 smart;新增 _low_uv.png 线框观察图。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:50:08 +08:00

133 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 在已有岛上重排,失败保底内置布局。
实测 gargoyle499952→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 注明。