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

7.5 KiB
Raw Blame History

UV 展开质量优化 + 线框观察图导出

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

背景与动机

减面工具当前 UV 流程(bl_decimate.py):

  • _unwrap_seam:锐边(66°)标 seam → MINIMUM_STRETCH(SLIM) 展开 → 内置 pack_islands
  • _do_unwrap:seam 结果过质量门(翻转 ≤2%、重叠 ≤8%)则用;一次不过就整个回退 _unwrap_smartSmart 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_seambl_decimate.py:128)在 unwrap 之后、pack 之前插两步:

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_unwrapbl_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 = 1024UV_PREVIEW_OPACITY = 0.25
  • 需要正确上下文:对象激活、edit mode、UV 全选。实现时验证 headless 可行性。
  • 输出路径:紧邻 _low.fbx,命名 <名>_low_uv.png(由 out_fbx 派生:同目录、os.path.splitext(out_fbx)[0] + "_uv.png")。
  • 失败:try/exceptRuntimeError/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)追加一行:

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 注明。