Files
AIC-Project/docs/superpowers/plans/2026-07-21-uvpm-packing-utilization.md
T

262 lines
13 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.
# UVPM 排布利用率优化实现计划(旋转 + 启发式搜索)
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:**`_uvpm_repack` 中开启 UVPM4 的旋转与启发式搜索,把薄条 UV 岛塞得更紧,明显提升 gargoyle 3000 的 UV 利用率而不退化其他指标。
**Architecture:** 改动集中在 `bl_decimate.py``_uvpm_repack`Blender 脚本)。因 UVPM4 属性名/默认值随版本可能不同,先用一次性探针脚本确认确切属性名与取值,再据实设置;属性设置走防御式 `_uvpm_set``hasattr` 才设),单个属性名不匹配只跳过、不拖垮整个 UVPM 排布。无纯函数可 TDD,靠探针 + 端到端验证(沿用本仓库约定)。
**Tech Stack:** Python 3、Blender 5.0 headless`bpy`)、UVPackmaster 4 扩展。
---
## File Structure
- `Tools/ModelTranslator/bl_decimate.py`Modify):新增常量 `UVPM_ROTATION_STEP`/`UVPM_HEURISTIC_TIME` 与辅助 `_uvpm_set`;在 `_uvpm_repack` 的 margin 设置后、pack 前开启旋转+启发式。
- `Tools/ModelTranslator/README.md`Modify):注明 UVPM 排布已开旋转+启发式。
- `Tools/ModelTranslator/tests/bl_probe_uvpm.py`Create then Delete):一次性探针,不入库。
---
## Task 1: UVPM4 属性探针(spike
确认 UVPM4 `default_main_props` 的旋转/启发式属性名、默认值、类型,并验证 headless 下开启后能跑通 pack。结果决定 Task 2 的确切属性名与取值。
**Files:**
- Create: `Tools/ModelTranslator/tests/bl_probe_uvpm.py`(用完删除,不入库)
- [ ] **Step 1: 写探针脚本**
Create `Tools/ModelTranslator/tests/bl_probe_uvpm.py`:
```python
"""探针:枚举 UVPM4 default_main_props 的旋转/启发式/边距属性,并在测试网格上
验证 headless 开启后能跑通 pack。运行:
blender -b --factory-startup --python tests/bl_probe_uvpm.py"""
import bpy
import addon_utils
# headless GPU 补丁(同 _uvpm_enableUVPM 导入期建视口 shader 会 SystemError
import gpu
_orig = gpu.shader.from_builtin
def _safe(*a, **k):
try:
return _orig(*a, **k)
except SystemError:
return None
gpu.shader.from_builtin = _safe
UVPM_EXT = "bl_ext.user_default.uvpackmaster4"
if addon_utils.enable(UVPM_EXT, default_set=True) is None:
print("UVPM_PROBE {'error': 'enable failed'}")
raise SystemExit
p = bpy.context.scene.uvpm4_props.default_main_props
# 枚举与 旋转/启发式/搜索/边距 相关的属性名 + 当前值
keys = ("rot", "heurist", "search", "margin", "iter")
props = {}
for n in dir(p):
if n.startswith("_"):
continue
if any(k in n.lower() for k in keys):
try:
props[n] = repr(getattr(p, n))
except Exception as e:
props[n] = "ERR:%r" % e
print("UVPM_PROPS " + repr(props))
# 测试 packUV 球 smart_project,尽力开启旋转/启发式,跑一次 pack
bpy.ops.mesh.primitive_uv_sphere_add()
obj = bpy.context.view_layer.objects.active
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.uv.smart_project()
def try_set(name, val):
if not hasattr(p, name):
return "absent"
try:
setattr(p, name, val)
return "ok=%r" % getattr(p, name)
except Exception as e:
return "ERR:%r" % e
tried = {
"rotation_enable": try_set("rotation_enable", True),
"rotation_step": try_set("rotation_step", 90),
"heuristic_enable": try_set("heuristic_enable", True),
"heuristic_search_time": try_set("heuristic_search_time", 3),
"heuristic_max_wait_time": try_set("heuristic_max_wait_time", 3),
}
bpy.context.scene.tool_settings.use_uv_select_sync = True
try:
ret = bpy.ops.uvpackmaster4.pack(mode_id='pack.single_tile', pack_op_type='0')
tried["pack"] = sorted(ret)
except Exception as e:
tried["pack"] = "ERR:%r" % e
finally:
bpy.ops.object.mode_set(mode='OBJECT')
print("UVPM_TRY " + repr(tried))
```
- [ ] **Step 2: 运行探针**
Run:
```bash
cd "d:/UD/AI/AIC#Project/Tools/ModelTranslator" && "D:\tools\blender-5.0.0-windows-x64\blender.exe" -b --factory-startup --python tests/bl_probe_uvpm.py 2>&1 | grep -E "UVPM_PROPS|UVPM_TRY|UVPM_PROBE"
```
Expected: 两行 `UVPM_PROPS {...}`(可用属性名+默认值)与 `UVPM_TRY {...}`(各属性设置结果 + `pack` 返回 `['FINISHED']`)。
- [ ] **Step 3: 记录结论**
从输出确认并记录:
- 旋转属性的确切名(`rotation_enable` 是否存在?步进属性名与类型/取值范围,如 `rotation_step` 是 int 度数还是 enum)。
- 启发式属性的确切名(`heuristic_enable`?时间上限是 `heuristic_search_time` 还是 `heuristic_max_wait_time`?单位/类型)。
- `pack` 是否返回 `FINISHED`(确认 headless 可跑)。
- 若启发式需要非 `'0'``pack_op_type` 或专门 mode,记录之。
这些结论用于 Task 2 的确切属性名与常量取值。
- [ ] **Step 4: 删除探针脚本**
```bash
cd "d:/UD/AI/AIC#Project/Tools/ModelTranslator" && rm tests/bl_probe_uvpm.py
```
(探针一次性、不入库;结论写入 Task 2 的 commit 正文。)
---
## Task 2: `_uvpm_repack` 开启旋转 + 启发式
新增防御式 `_uvpm_set` 辅助与常量,在 `_uvpm_repack` 的 margin 设置后、pack 前开启旋转与启发式搜索。**属性名与取值以 Task 1 探针结论为准**——下方代码用最可能的名字,执行时按探针实测替换/校正。
**Files:**
- Modify: `Tools/ModelTranslator/bl_decimate.py`(常量区 + `_uvpm_repack` 之前新增 `_uvpm_set` + `_uvpm_repack` 内部)
**Task 1 探针结论(已确认,用于本任务)**`default_main_props``rotation_enable`(bool, 默认 True)、`rotation_step`(int 度, 默认 90)、`heuristic_enable`(bool, 默认 False)、`heuristic_search_time`(int 秒, 默认 0=无限) 均存在且可设;旋转默认已开、pre-rotation 默认开(`pre_rotation_disable=False`);`pack` 返回 `{'FINISHED','PASS_THROUGH'}`,现有 `if 'FINISHED' not in ret` 判定已兼容;无需改 `pack_op_type`。真正需显式开的是 **heuristic**,并把 rotation_step 调细以利薄斜条对齐。
- [ ] **Step 1: 新增常量**
`bl_decimate.py` 常量区 `UVPM_TEX_SIZE = 2048` 那一行之后追加:
```python
UVPM_ROTATION_STEP = 15 # UVPM 旋转步进(度);比默认 90 更细,利于薄斜条对齐
UVPM_HEURISTIC_TIME = 10 # UVPM 启发式搜索秒数上限(默认 0=无限,必须给正值上限)
```
- [ ] **Step 2: 新增 `_uvpm_set` 辅助**
`bl_decimate.py``_uvpm_repack` 函数**之前**新增:
```python
def _uvpm_set(p, name, value, warnings):
"""防御式设 UVPM 属性:属性存在才设,否则记警告跳过——
名字不匹配(UVPM 版本差异)时降级为按原 margin 跑 UVPM,不丢整个排布。"""
if hasattr(p, name):
try:
setattr(p, name, value)
except Exception as e:
warnings.append("UVPM 属性 %s 设置失败(%s),跳过" % (name, e))
else:
warnings.append("UVPM 属性 %s 不存在,跳过" % name)
```
- [ ] **Step 3: 在 `_uvpm_repack` 开启旋转 + 启发式**
`_uvpm_repack` 中,现有三行 margin 设置(`p.pixel_margin_enable = True` / `p.pixel_margin = UVPM_PIXEL_MARGIN` / `p.pixel_margin_tex_size = UVPM_TEX_SIZE`)之后、`bpy.context.scene.tool_settings.use_uv_select_sync = True` 之前,插入(属性名以 Task 1 为准替换):
```python
_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)
```
注意:
- 属性名已由 Task 1 探针确认全部存在(`rotation_enable`/`rotation_step`/`heuristic_enable`/`heuristic_search_time`)——用上面这四行即可,无需改名。
- `pack_op_type` 保持 `'0'`(探针确认启发式开启下 `'0'` 正常返回 FINISHED,无需改)。
- 这些设置在现有 `_uvpm_repack``try/except Exception` 内;`_uvpm_set``hasattr` 守卫确保单属性问题不触发整体回退。
- [ ] **Step 4: 语法自检 + 单测不回归**
Run:
```bash
cd "d:/UD/AI/AIC#Project/Tools/ModelTranslator" && python -c "import ast; ast.parse(open('bl_decimate.py', encoding='utf-8').read()); print('OK')" && python -m unittest tests.test_bl_decimate 2>&1 | tail -3
```
Expected: `OK`,随后单测全 PASS(本改动无纯函数,仅确认无破坏)。(排布效果在 Task 3 端到端验证。)
- [ ] **Step 5: 提交**
```bash
cd "d:/UD/AI/AIC#Project" && git add Tools/ModelTranslator/bl_decimate.py && git commit -m "ModelTranslator: _uvpm_repack 开启旋转+启发式搜索(防御式设属性)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
(在 commit 正文补一句 Task 1 探针确认的确切属性名/取值,便于追溯。)
---
## Task 3: 端到端验证 + README
**Files:**
- Modify: `Tools/ModelTranslator/README.md`
- [ ] **Step 1: 全量单测**
Run:
```bash
cd "d:/UD/AI/AIC#Project/Tools/ModelTranslator" && python -m unittest tests.test_bl_decimate tests.test_uv_preview tests.test_unity_assets 2>&1 | tail -3
```
Expected: 全部 PASS。
- [ ] **Step 2: 端到端主验收(gargoyle 3000+ 计时**
Run:
```bash
cd "d:/UD/AI/AIC#Project/Tools/ModelTranslator" && rm -f src/gargoyle_low_uv.png && PYTHONIOENCODING=utf-8 python -c "import time,subprocess,sys; t=time.time(); r=subprocess.run([sys.executable,'model_decimate.py','src/gargoyle.fbx','--tris','3000','--max-overlap','0.15'],capture_output=True,text=True,encoding='utf-8'); print(r.stdout[-800:]); print('ELAPSED %.1fs'%(time.time()-t))"
```
Expected: `模式=seam@45+uvpm`;岛数≈390(排布不改岛数);**利用率明显高于基线 52.3%**;翻转 0%;重叠 ≤13.3%(不高于基线)。记录利用率与 `ELAPSED` 秒数(pack 增量)。若有 `UVPM 属性 … 不存在` 警告说明属性名未对齐,需回 Task 2 用探针名校正。
- [ ] **Step 3: 观察图肉眼确认**
用 Read 工具打开 `src/gargoyle_low_uv.png`,确认薄条岛塞得更紧、空白(黑块)区域明显减少。
- [ ] **Step 4: 回归 well1500(正常 seam@66 情形)**
Run:
```bash
cd "d:/UD/AI/AIC#Project/Tools/ModelTranslator" && PYTHONIOENCODING=utf-8 python model_decimate.py src/well1500.fbx --tris 5000 2>&1 | tail -4
```
Expected: 成功,无异常退出;`模式=seam@NN+uvpm`;利用率不低于其历史水平(排布增强不应变差)。
- [ ] **Step 5: 更新 README**
`Tools/ModelTranslator/README.md``- **model_decimate.py**:…` 那条中,把 `排布默认用 UVPackmaster 4 重排(只在最终选定 UV 上跑一次)` 这一处描述扩写为:
```markdown
排布默认用 UVPackmaster 4 重排(只在最终选定 UV 上跑一次,已开旋转+启发式搜索以提升薄条岛的利用率)
```
(只改这一处短语,不动其余。)
- [ ] **Step 6: 提交**
```bash
cd "d:/UD/AI/AIC#Project" && git add Tools/ModelTranslator/README.md && git commit -m "ModelTranslator: README 注明 UVPM 已开旋转+启发式搜索
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
- [ ] **Step 7: 汇报前后对比**
在最终汇报里给出:基线(seam@45 / 390 岛 / 52.3% / 翻转 0% / 重叠 13.3%)vs 现在(利用率 / 岛数 / 翻转 / 重叠 / pack 耗时),并附观察图观察结论。
---
## Self-Review 记录
- **Spec 覆盖**:①探针确认属性名→Task 1;②`_uvpm_repack` 开旋转+启发式(防御式 `_uvpm_set` + 常量)→Task 2;③验证(gargoyle 主验收 + well1500 回归 + 观察图)→Task 3 Step 2-4README→Task 3 Step 5;错误处理(属性名不匹配跳过、整体失败回退、启发式时间上限)→Task 2 `_uvpm_set` 守卫 + 常量。全部有对应任务。
- **占位符**:探针脚本、`_uvpm_set`、常量、各命令均为完整内容。属性名标注"以 Task 1 为准"是 spike-first 设计的必要性质(执行时由 Task 1 输出定稿),非未决占位——`hasattr` 守卫保证即便名字有出入也不崩,Task 3 Step 2 的"属性不存在"警告会兜住并要求回改。
- **类型/命名一致**`_uvpm_set(p, name, value, warnings)`Task 2 Step 2 定义,Step 3 调用一致);常量 `UVPM_ROTATION_STEP`/`UVPM_HEURISTIC_TIME`Task 2 Step 1 定义,Step 3 使用一致);`p``scene.uvpm4_props.default_main_props`(与现有 `_uvpm_repack` 一致)。
- **已知风险**UVPM4 属性名/取值由 Task 1 探针定;启发式若需特定 `pack_op_type` 在 Task 2 据实调整;薄条几何天然限制上限,验收以"明显上升"而非硬数字(spec 一致)。