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>
8.1 KiB
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 线框观察图便于人工查阅。
目标
- 展开阶段引入拉伸松弛与纹素密度均衡,降低翻转/拉伸。
- seam 失败不再一步退化为碎岛的 smart,而是先松弛、再按更低角度扫描抢救,尽量保住低岛数解析式展开。
- 每次减面产出一张 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.0;Blender 自带 Python 无 PIL)。
设计
1. 展开质量增强(minimize_stretch + average_islands_scale)
_unwrap_seam(bl_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_scaleheadless 上下文若不可用: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)改为分级:
- seam@66° → 松弛 → 采集指标 → 测门,过则采用;
- 不过 → 依次试
SEAM_ANGLE_SWEEP = (55, 45, 35),每档:clear UV → 标 seam@角度 → 展开+松弛 → 采集指标;在所有过门候选里取 UV 岛数最少者采用; - 全不过门 →
_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_polysJSON → 调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)追加一行:
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 必产出。
测试与验证
- 现有单测不回归:
python -m unittest tests.test_unity_assets。 - 纯函数可单测:
SEAM_ANGLE_SWEEP遍历顺序、候选择优(取过门且岛最少)逻辑抽成纯函数pick_best_candidate(candidates)便于单测(输入指标 dict 列表,输出选中项)。 - 端到端:
python model_decimate.py src/gargoyle.fbx --tris 1000,对比改动前后指标(岛数/利用率/翻转/重叠)与mode标注;确认src/gargoyle_low_uv.png生成且线框可读。 - 回归另一模型(如 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 注明。