Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.6 KiB
UVPM 排布利用率优化:开启旋转 + 启发式搜索
日期:2026-07-21
范围:Tools/ModelTranslator/bl_decimate.py(_uvpm_repack)、README.md
背景与动机
减岛优化后 gargoyle 3000 面走 seam@45、390 岛,但 UV 利用率仅 52.3%——约 48% 为空白(烘焙时填黑,用户观感为"错误的黑色块")。
根因定位(已查 _uvpm_repack bl_decimate.py:286-314):
- 当前 UVPM 调用只设
pixel_margin(4px@2048),旋转/启发式搜索全用 UVPM4 默认值:bpy.ops.uvpackmaster4.pack(mode_id='pack.single_tile', pack_op_type='0')。 - gargoyle 减面后 UV 由大量又长又斜的薄条岛(尖刺/薄翅缘几何)主导。薄斜条在缺乏精细旋转对齐时外接矩形浪费极大——这是 packer 效率的天敌。
- UVPM 的旋转 + 启发式迭代搜索正是为此类不规则岛设计的,当前未启用。
用户选择:只调 UVPM 排布(不碰边距/烘焙),启发式耗时可接受(离线工具,设时间上限)。
目标
开启 UVPM4 的旋转与启发式搜索,把薄条岛塞得更紧,明显提升 gargoyle 3000 的 UV 利用率,且不退化其他指标(岛数、翻转、重叠不变差)。
非目标(YAGNI)
- 不改像素边距
pixel_margin(4px)与model_bake的烘焙 padding(避免渗色,用户明确排除)。 - 不改展开/减岛/清理逻辑(
_do_unwrap、_clean_mesh、--max-overlap均不动)。 - 不动
average_islands_scale(纹素均匀,保留)。 - 不追求硬性利用率数字(薄条几何天然受限),只求"明显上升"。
设计
1. UVPM4 属性探针(实现首步 spike)
因 UVPM4 属性名/默认值随版本可能不同,先用一次性脚本确认,避免猜 API:
- 启用 UVPM4(复用
_uvpm_enable的 headless GPU 补丁思路)。 - 枚举
bpy.context.scene.uvpm4_props.default_main_props中旋转/启发式相关属性:名字、默认值、类型/取值范围(候选:rotation_enable、rotation_step、rotation_step_value、heuristic_enable、heuristic_search_time、heuristic_max_wait_time等)。 - 在测试网格(UV 球 + smart_project)上开启这些属性跑一次
uvpackmaster4.pack,确认 headless 不报错、返回FINISHED。 - 产出:确切属性名与合理取值,供第 2 节定稿;脚本用完删除、不入库。
2. _uvpm_repack 开启旋转 + 启发式
新增防御式辅助与常量,在 _uvpm_repack(bl_decimate.py:294-305)的 pixel_margin 设置之后、pack 调用之前设置属性:
# 常量(值以探针结论为准,示意)
UVPM_ROTATION_STEP = <探针定> # 旋转步进(度),更细利于薄条对齐
UVPM_HEURISTIC_TIME = <探针定> # 启发式搜索秒数上限(如 5–10)
def _uvpm_set(p, name, value, warnings):
"""防御式设 UVPM 属性:属性存在才设,否则记警告跳过——
名字不匹配(版本差异)时降级为按原 margin 跑 UVPM,不丢整个排布。"""
if hasattr(p, name):
setattr(p, name, value)
else:
warnings.append("UVPM 属性 %s 不存在,跳过" % name)
在 _uvpm_repack 内(属性名以探针为准):
_uvpm_set(p, "rotation_enable", True, warnings)
_uvpm_set(p, "rotation_step", UVPM_ROTATION_STEP, warnings)
_uvpm_set(p, "heuristic_enable", True, warnings)
_uvpm_set(p, "heuristic_search_time", UVPM_HEURISTIC_TIME, warnings)
- 属性设置在现有
try/except Exception(bl_decimate.py:293-314)内——但用_uvpm_set的hasattr守卫,使单个属性名不匹配只跳过该项、不抛异常,从而不会触发整体失败回退(那会丢掉 UVPM 全部排布)。 - 旋转后的 UV 对烘焙无影响(烘焙按 UV 原样采样)。
- 边距、
pack调用签名(mode_id/pack_op_type)保持不变,除非探针发现启发式必须换pack_op_type(若如此,在第 2 节据实调整并记录)。
3. 数据流(不变,仅 UVPM 内部行为增强)
… → _do_unwrap → 选定 UV → _uvpm_repack(现在:margin + 旋转 + 启发式搜索)→ 抽 UV 几何 → export
错误处理
| 失败点 | 处理 |
|---|---|
| UVPM 某属性名不存在(版本差异) | _uvpm_set 记警告、跳过该属性,其余照设、pack 照跑 |
| UVPM 启用/pack 整体失败 | 现有 _uvpm_repack try/except:记警告、_uvpm_failed=True、回退内置 pack(行为不变) |
| 启发式耗时过长 | 由 UVPM_HEURISTIC_TIME 上限约束 |
原则:排布增强单点失败降级,不影响主流程与 FBX 产出。
测试与验证
- 现有单测不回归:
python -m unittest tests.test_bl_decimate tests.test_uv_preview tests.test_unity_assets(本改动无纯函数,主要确认无破坏)。 - 端到端(主验收):
python model_decimate.py src/gargoyle.fbx --tris 3000 --max-overlap 0.15,对比基线(seam@45 / 390 岛 / 利用率 52.3% / 翻转 0% / 重叠 13.3%):- 利用率明显上升;
- 岛数不变(排布不改岛数)、翻转仍 0%、重叠仍 ≤ 门限;
- 记录 pack 耗时增量;
- Read 观察图确认薄条更紧、黑块变少。
- 回归:
python model_decimate.py src/well1500.fbx --tris 5000(正常走 seam@66 的模型)确认排布增强不破坏,流程不崩、产物正常。
验收标准
- gargoyle 3000(
--max-overlap 0.15):UV 利用率明显高于 52.3%;岛数/翻转/重叠不退化。 - 属性名不匹配时降级不崩、仍出 UVPM 排布(或内置兜底)。
- well1500 回归正常。
- 现有单测全绿。
实现注意
- 属性名/取值一律以第 1 节探针结论为准;spike 未确认前不写死具体名。
_uvpm_set的hasattr守卫是关键:防止版本差异把整个 UVPM 排布拖垮。- 若探针发现启发式需要非
'0'的pack_op_type或专门mode_id,在计划中据实调整并在 README 注明。
实现修订(2026-07-21,系统调试后)
实现中发现 UVPM 启发式搜索在 headless 下会随机崩溃引擎进程(Error: Engine process died unexpectedly):well1500 崩 3/3、gargoyle 崩 1/2(同模型重跑结果不一致),旋转步进无辜。启发式因此不可靠、且使输出非确定。
最终落地:弃用启发式,只保留 rotation_step=15(稳定、零崩溃、确定性)。实测利用率提升较小但可靠——gargoyle 3000(--max-overlap 0.15)52.3%→55.6%,well1500 5000 稳定 +uvpm。移除 _uvpm_pack_once/重试与 UVPM_HEURISTIC_TIME;_uvpm_repack 回到单次 UVPM(rotation 15)。验收从"明显上升"下修为"稳定小幅上升且零崩溃/确定性"。