Files
AIC-Project/docs/superpowers/specs/2026-07-21-uv-unwrap-optimization-design.md
T
ud18010andClaude Opus 4.8 f0046ea64d ModelTranslator: 按 spike 结论改 UV 观察图为 PIL 两端架构
headless Blender export_layout(PNG) 走 GPUOffScreen 不可用;改为
Blender 抽 UV 几何→sidecar JSON→系统 Python(Pillow) 绘 PNG。
拆 Task 5(绘图 TDD)/6(抽几何)/7(渲染清理),README 加 Pillow 依赖。

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

132 lines
8.1 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 等新依赖。~~ **2026-07-21 修订)** headless Blender 的 `export_layout(mode='PNG')` 走 GPUOffScreen,后台模式不可用(`SystemError: GPU functions ... not available in background mode`);`mode='SVG'` 可用但非位图。经用户确认,观察图改为**用系统 Python 的 Pillow 自绘 PNG 线框**——引入 Pillow 依赖(系统 Python 已装 12.2.0Blender 自带 Python 无 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 线框观察图(Blender 抽几何 → 系统 Python 用 PIL 绘 PNG
因 headless 无法用 Blender 直接出 PNG(见非目标修订),拆成两端:
**Blender 端(`bl_decimate.py`**:最终 UV 定下后,`_collect_uv_polygons(obj)` 用 bmesh 提取每个面的 UV 多边形(归一化 0-1 坐标)`[[[u,v],...], ...]``main()` 写入 sidecar JSON `<名>_low_uv.polys.json``MT_SUMMARY` 增加字段 `uv_preview`(目标 PNG 路径 `<名>_low_uv.png`)与 `uv_polys`sidecar JSON 路径);抽取失败记警告、两字段置 null、不中断。
**系统 Python 端(`uv_preview.py` + `model_decimate.py`**
- 新模块 `uv_preview.py` 提供纯函数 `render_uv_png(polygons, out_png, size=1024, pad=8, ...)`:白底、半透明填充 + 深色线框(V 轴翻转到图像坐标),用 Pillow 绘制并存 PNG。可在系统 Python 单测。
- `model_decimate.py` 拿到 summary 后:读 `uv_polys` JSON → 调 `render_uv_png` 输出到 `uv_preview` → 删除 sidecar JSON → 打印 PNG 路径;任一步失败打印警告、不中断(FBX 已产出)。
- 输出:紧邻 `_low.fbx`,命名 `<名>_low_uv.png`
### 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 注明。