Files
AIC-Project/docs/superpowers/plans/2026-07-16-decimate-uv-seam-unwrap.md
T
2026-07-16 11:50:38 +08:00

404 lines
15 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.
# 减面工具 UV 展开优化实现计划
> **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:** `model_decimate.py` 的 UV 展开从 Smart UV Project 换成"锐边 seam + MINIMUM_STRETCH 展开 + 质量门自动回退",大幅减少 UV 碎岛。
**Architecture:** 纯逻辑(union-find 数岛、翻转/利用率计算)放 `bl_decimate.py` 顶部可单测区;Blender 侧新增 seam 展开路径与 bmesh 指标提取,质量门(翻转/重叠占比 >2%)触发时回退原 smart_project 路径。`MT_SUMMARY` 增加 `uv` 字段,CLI 增加 `--unwrap {seam,smart}`(默认 seam)。
**Tech Stack:** Python 3 标准库、Blender 5.0bpy/bmesh)、unittest。
**设计文档:** `docs/superpowers/specs/2026-07-16-decimate-uv-seam-unwrap-design.md`
**重要提醒:**
- 仓库路径含 `#``D:\UD\AI\AIC#Project`),**bash 里所有路径必须加引号**
- Blender`D:\tools\blender-5.0.0-windows-x64\blender.exe`
- 冒烟素材:`Tools/ModelTranslator/src/well1500.fbx`149.9 万面)→ 5000 面
- 产物(`_low.fbx`)不入 git,只提交代码/文档
## 文件结构
```
Tools/ModelTranslator/
bl_decimate.py # 修改:纯逻辑区加 3 函数+1 常量;Blender 区重构展开路径
model_decimate.py # 修改:--unwrap 参数 + UV 指标打印
tests/test_bl_decimate.py # 修改:追加纯逻辑测试
README.md # 修改:--unwrap 说明(Task 3
```
---
### Task 1: UV 指标纯逻辑(TDD
**Files:**
- Modify: `Tools/ModelTranslator/tests/test_bl_decimate.py`(文件末尾 `if __name__` 之前追加)
- Modify: `Tools/ModelTranslator/bl_decimate.py`(纯逻辑区,`within_tolerance` 之后、`# ---------------- 以下仅在 Blender 内执行` 之前)
- [ ] **Step 1: 追加失败测试**
`tests/test_bl_decimate.py``if __name__ == "__main__":` 之前插入:
```python
class TestCountIslands(unittest.TestCase):
def test_no_faces(self):
self.assertEqual(bd.count_islands(0, []), 0)
def test_no_pairs_each_face_is_island(self):
self.assertEqual(bd.count_islands(3, []), 3)
def test_chain_merges_to_one(self):
self.assertEqual(bd.count_islands(4, [(0, 1), (1, 2), (2, 3)]), 1)
def test_two_groups(self):
self.assertEqual(bd.count_islands(5, [(0, 1), (3, 4)]), 3) # {0,1} {2} {3,4}
def test_duplicate_pairs_ok(self):
self.assertEqual(bd.count_islands(2, [(0, 1), (1, 0), (0, 1)]), 1)
class TestFlippedFraction(unittest.TestCase):
def test_empty_is_zero(self):
self.assertEqual(bd.flipped_fraction([]), 0.0)
def test_mixed(self):
self.assertAlmostEqual(bd.flipped_fraction([0.1, -0.2, 0.3, 0.4]), 0.25)
def test_all_positive(self):
self.assertEqual(bd.flipped_fraction([0.1, 0.2]), 0.0)
class TestFillRatio(unittest.TestCase):
def test_sum(self):
self.assertAlmostEqual(bd.fill_ratio([0.2, 0.3]), 0.5)
def test_clamped_to_one(self):
self.assertEqual(bd.fill_ratio([0.8, 0.9]), 1.0)
def test_empty_is_zero(self):
self.assertEqual(bd.fill_ratio([]), 0.0)
```
- [ ] **Step 2: 运行确认失败**
Run: `cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && python -m unittest tests.test_bl_decimate -v`
Expected: ERROR`module 'bl_decimate' has no attribute 'count_islands'` 等)
- [ ] **Step 3: 写实现**
`bl_decimate.py``within_tolerance` 函数之后、`# ---------------- 以下仅在 Blender 内执行` 注释之前插入:
```python
UV_BAD_TOL = 0.02 # 质量门:UV 翻转/重叠面占比超此值则回退 smart_project
def count_islands(n_faces, adjacent_pairs):
"""union-find 数 UV 岛:n_faces 个面,adjacent_pairs 为 UV 连续的面索引对。"""
parent = list(range(n_faces))
def find(i):
while parent[i] != i:
parent[i] = parent[parent[i]]
i = parent[i]
return i
for a, b in adjacent_pairs:
ra, rb = find(a), find(b)
if ra != rb:
parent[ra] = rb
return len({find(i) for i in range(n_faces)})
def flipped_fraction(signed_areas):
"""UV 面签名面积为负(翻转)的占比。"""
if not signed_areas:
return 0.0
return sum(1 for a in signed_areas if a < 0) / float(len(signed_areas))
def fill_ratio(abs_areas):
"""UV 空间利用率:面积和(重叠会虚高,截断到 1.0)。"""
return min(1.0, sum(abs_areas))
```
- [ ] **Step 4: 运行确认通过**
Run: `cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && python -m unittest tests.test_bl_decimate -v`
Expected: 19 个测试全部 PASS(原 8 + 新 11
- [ ] **Step 5: Commit**
```bash
cd "D:/UD/AI/AIC#Project" && git add Tools/ModelTranslator/bl_decimate.py Tools/ModelTranslator/tests/test_bl_decimate.py && git commit -m "ModelTranslator: UV 岛数/翻转/利用率纯逻辑(TDD)"
```
---
### Task 2: seam 展开路径 + 质量门 + CLI 参数
**Files:**
- Modify: `Tools/ModelTranslator/bl_decimate.py`Blender 区:替换 `_smart_unwrap``main()` 接线)
- Modify: `Tools/ModelTranslator/model_decimate.py``--unwrap` + UV 指标打印)
- [ ] **Step 1: bl_decimate.py 替换 `_smart_unwrap` 为新展开体系**
把现有 `_smart_unwrap` 函数(`def _smart_unwrap(obj):` 到其 `bpy.ops.object.mode_set(mode='OBJECT')` 整个函数)整体替换为:
```python
SEAM_ANGLE_DEG = 66.0 # 锐边阈值:两面夹角超此值标 seam
PACK_MARGIN = 0.002 # 岛间距 ≈ 2048 图 4px
def _clear_uv_layers(mesh):
while mesh.uv_layers:
mesh.uv_layers.remove(mesh.uv_layers[0])
def _unwrap_smart(obj):
"""Smart UV Project(原路径,也是质量门的回退路径)。"""
import bpy
import math
_clear_uv_layers(obj.data)
bpy.context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.uv.smart_project(angle_limit=math.radians(66.0), island_margin=PACK_MARGIN)
bpy.ops.object.mode_set(mode='OBJECT')
def _unwrap_seam(obj, warnings):
"""锐边标 seam -> MINIMUM_STRETCH/ANGLE_BASED 展开 -> pack。碎岛远少于投影式。"""
import bpy
import math
_clear_uv_layers(obj.data)
obj.data.uv_layers.new()
bpy.context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode='EDIT')
# 锐边标 seam:接缝只落在硬边处,平滑区域保持整岛
bpy.ops.mesh.select_mode(type='EDGE')
bpy.ops.mesh.select_all(action='DESELECT')
bpy.ops.mesh.edges_select_sharp(sharpness=math.radians(SEAM_ANGLE_DEG))
bpy.ops.mesh.mark_seam(clear=False)
bpy.ops.mesh.select_all(action='SELECT')
try:
bpy.ops.uv.unwrap(method='MINIMUM_STRETCH', margin=PACK_MARGIN)
except TypeError: # 老版本无 SLIM 枚举
warnings.append("MINIMUM_STRETCH 不可用,展开改用 ANGLE_BASED")
bpy.ops.uv.unwrap(method='ANGLE_BASED', margin=PACK_MARGIN)
try:
bpy.ops.uv.pack_islands(rotate=True, margin=PACK_MARGIN)
except RuntimeError: # headless 上下文不满足时靠 unwrap 自带打包
warnings.append("pack_islands 不可用,沿用 unwrap 自带布局")
bpy.ops.object.mode_set(mode='OBJECT')
def _collect_uv_metrics(obj):
"""bmesh 提取 UV 指标:岛数(UV 连续边并查集)、利用率、翻转占比、重叠占比。"""
import bmesh
bm = bmesh.new()
bm.from_mesh(obj.data)
uv = bm.loops.layers.uv.active
pairs = []
for e in bm.edges:
if len(e.link_faces) != 2:
continue
f1, f2 = e.link_faces
cont = True
for v in e.verts: # 两面在该边两端 UV 一致 => UV 连续(同岛)
l1 = next(l for l in f1.loops if l.vert == v)
l2 = next(l for l in f2.loops if l.vert == v)
if (l1[uv].uv - l2[uv].uv).length > 1e-6:
cont = False
break
if cont:
pairs.append((f1.index, f2.index))
signed = []
for f in bm.faces:
area = 0.0
loops = f.loops
for i in range(len(loops)):
a = loops[i][uv].uv
b = loops[(i + 1) % len(loops)][uv].uv
area += a.x * b.y - b.x * a.y
signed.append(area * 0.5)
n = len(bm.faces)
bm.free()
return {"islands": count_islands(n, pairs),
"fill": round(fill_ratio([abs(a) for a in signed]), 4),
"flipped": round(flipped_fraction(signed), 4),
"overlap": _overlap_fraction(obj)}
def _overlap_fraction(obj):
"""uv.select_overlap 统计重叠 UV 面占比;op 上下文不可用返回 None。"""
import bpy
bpy.context.scene.tool_settings.use_uv_select_sync = True
bpy.context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='DESELECT')
try:
bpy.ops.uv.select_overlap()
except RuntimeError:
return None
finally:
bpy.ops.object.mode_set(mode='OBJECT')
polys = obj.data.polygons
if not len(polys):
return 0.0
return round(sum(1 for p in polys if p.select) / float(len(polys)), 4)
def _do_unwrap(obj, mode, warnings):
"""按模式展开;seam 质量不达标自动回退 smart。返回 uv 指标 dict(含 mode)。"""
if mode == "seam":
_unwrap_seam(obj, warnings)
m = _collect_uv_metrics(obj)
overlap = m["overlap"] or 0.0
if m["flipped"] <= UV_BAD_TOL and overlap <= UV_BAD_TOL:
m["mode"] = "seam"
return m
warnings.append("seam 展开质量不达标(翻转 %.1f%% 重叠 %s),回退 smart_project"
% (m["flipped"] * 100,
"%.1f%%" % (m["overlap"] * 100) if m["overlap"] is not None else "未知"))
mode = "smart_fallback"
_unwrap_smart(obj)
m = _collect_uv_metrics(obj)
m["mode"] = mode if mode == "smart_fallback" else "smart"
return m
```
- [ ] **Step 2: main() 接线(argv 第 4 参 + uv 字段)**
`main()` 里两处修改。argv 解析行:
```python
src, out_fbx, target = argv[0], argv[1], int(argv[2])
```
改为:
```python
src, out_fbx, target = argv[0], argv[1], int(argv[2])
unwrap_mode = argv[3] if len(argv) > 3 else "seam"
```
`_smart_unwrap(obj)` 调用行改为:
```python
uv_info = _do_unwrap(obj, unwrap_mode, warnings)
```
`MT_SUMMARY` 的 dict 加 `"uv": uv_info`
```python
print("MT_SUMMARY " + json.dumps(
{"src": os.path.basename(src), "fbx": os.path.basename(out_fbx),
"tris_before": orig, "tris_after": cur, "target": target,
"uv": uv_info, "warnings": warnings}, ensure_ascii=False))
```
同时把文件顶部 docstring 第 1-2 行更新为:
```python
"""Blender 内运行:FBX 减面到指定三角面数 + UV 重展(锐边 seam 或 Smart UV Project)。
调用:blender -b --factory-startup --python bl_decimate.py -- <src.fbx> <out.fbx> <target_tris> [seam|smart]
"""
```
- [ ] **Step 3: model_decimate.py 加 --unwrap 与指标打印**
argparse 增加(`--blender` 行之前):
```python
ap.add_argument("--unwrap", choices=("seam", "smart"), default="seam",
help="UV 展开方式:seam=锐边接缝整岛展开(默认),smart=Smart UV Project")
```
`run_blender_script` 调用改为:
```python
s = run_blender_script(blender, "bl_decimate.py",
[args.input, out_fbx, str(args.tris), args.unwrap])
```
结果打印行之后、警告循环之前加:
```python
u = s["uv"]
overlap = "%.1f%%" % (u["overlap"] * 100) if u["overlap"] is not None else "n/a"
print(" UV: 模式=%s 岛数=%d 利用率=%.1f%% 翻转=%.1f%% 重叠=%s" %
(u["mode"], u["islands"], u["fill"] * 100, u["flipped"] * 100, overlap))
```
文件 docstring 第 1 行的"并 Smart UV 重展"改为"并重展 UV(默认锐边 seam 整岛展开)"。
- [ ] **Step 4: 语法检查 + 全部单测**
Run: `cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && python -m py_compile bl_decimate.py model_decimate.py && python -m unittest discover -s tests -v`
Expected: 编译通过,40 个测试全部 PASS(29 原有 + 11 新增)
- [ ] **Step 5: Commit**
```bash
cd "D:/UD/AI/AIC#Project" && git add Tools/ModelTranslator/bl_decimate.py Tools/ModelTranslator/model_decimate.py && git commit -m "ModelTranslator: 减面 UV 改锐边 seam 展开(质量门回退 smart--unwrap 可选)"
```
---
### Task 3: 双模式冒烟对比 + README
**Files:**
- Modify: `Tools/ModelTranslator/README.md`
- 产物 `src/well1500_low.fbx``/tmp` 对比数据不入 git
- [ ] **Step 1: smart 模式跑基线**timeout 300000
Run: `cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && python model_decimate.py src/well1500.fbx --tris 5000 --unwrap smart`
Expected: 正常完成,记录 `UV: 模式=smart 岛数=N_smart 利用率=F_smart ...`
- [ ] **Step 2: seam 模式(默认)跑对比**timeout 300000
Run: `cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && python model_decimate.py src/well1500.fbx --tris 5000`
Expected: `UV: 模式=seam ...`,且满足验收标准:
- `岛数` 显著低于 smart 基线(至少降一个数量级或 <1/5)
- `利用率` ≥ smart 基线的 90%
- `翻转` ≤ 2%、`重叠` ≤ 2%(未触发回退警告)
不达标则按 systematic-debugging 排查(常见点:`edges_select_sharp` 的 sharpness 语义、`MINIMUM_STRETCH` 枚举名、`pack_islands`/`select_overlap` headless poll 失败——后两者已有降级路径,看警告判断),修复后重跑并把修复单独 commit。
- [ ] **Step 3: 用新低模快速复烘验证**timeout 600000
Run: `cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && python model_bake.py src/well1500.fbx src/well1500_low.fbx --size 1024 --samples 16`
Expected: 5 张 PNG 正常产出、无 error;用下面命令确认 normal/color 非空:
```bash
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && "D:/tools/blender-5.0.0-windows-x64/blender.exe" -b --factory-startup --python-expr "
import bpy
for n in ('color', 'normal'):
img = bpy.data.images.load(r'D:\UD\AI\AIC#Project\Tools\ModelTranslator\out_bake\well1500\well1500_%s.png' % n)
uniq = len(set(round(v, 2) for v in img.pixels[:4000]))
print('CHECK', n, 'unique=', uniq)
"
```
Expected: 两图 unique 明显 >1
- [ ] **Step 4: README 更新**
`README.md` 中 model_decimate.py 说明条目("旧 UV 全删,Smart UV Project 重展"那句)改为:
```
- **model_decimate.py**:多 mesh 自动 join;三角化后 Decimate(collapse) 减到 `--tris`(±3%,最多 2 轮修正);旧 UV 全删后重展——默认锐边标 seam + MINIMUM_STRETCH 整岛展开(碎岛少、接缝落在硬边),质量不达标(翻转/重叠 >2%)自动回退 Smart UV Project`--unwrap smart` 可强制旧行为;输出 `<名>_low.fbx``-o` 改目录),日志报 UV 岛数/利用率
```
- [ ] **Step 5: 全部单测最后过一遍 + Commit**
Run: `cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && python -m unittest discover -s tests -v`
Expected: 40/40 PASS
```bash
cd "D:/UD/AI/AIC#Project" && git add Tools/ModelTranslator/README.md && git commit -m "ModelTranslator: README 补 --unwrap 说明(well1500 双模式冒烟对比通过)"
```
- [ ] **Step 6: Unity 手动验证(用户操作)**
如需上 Unity 验证:`python model_translator.py out_bake/well1500/well1500.fbx` 后拷 `out/well1500/``Client/Assets/` 查看接缝表现。此步由用户完成。