Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
266 lines
13 KiB
Markdown
266 lines
13 KiB
Markdown
# 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_enable:UVPM 导入期建视口 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))
|
||
|
||
# 测试 pack:UV 球 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-4;README→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 一致)。
|
||
|
||
## 执行修订(2026-07-21)
|
||
|
||
Task 3 端到端验证暴露:**UVPM 启发式在 headless 下随机崩溃引擎**(well1500 3/3、gargoyle 1/2)。经 systematic-debugging 确认根因为 heuristic(rotation 无辜),且崩溃非确定。用户裁定:**弃用启发式,只留 rotation_step=15**。`_uvpm_repack` 移除重试/启发式,回到单次 UVPM(rotation 15)。最终:gargoyle 52.3→55.6%、well1500 稳定 +uvpm,零崩溃、确定性。
|