Files
AIC-Project/docs/superpowers/plans/2026-07-23-quad-reducer-retopo-upgrade.md
T
2026-07-23 15:09:24 +08:00

434 lines
19 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.
# `--reducer quad` 可靠四边重拓扑升级 Implementation Plan
> **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:**`--reducer quad` 升级为可靠四边重拓扑:QR 前预 collapse 到中等密度、补小洞、软水密门、保四边不三角化,让 quad 后端在高密度模型上也能产出干净四边低模。
**Architecture:** 纯策略(`topology_acceptable` 加容忍参数、`precollapse_target`)进 `qr_bridge.py` 可 TDDBlender 操作(预 collapse、`fill_holes`、保四边导出)进 `bl_decimate.py`,扩展 `_remesh_quad``main``model_decimate.py``--qr-input-cap` 透传。collapse 后端与默认值零变化。
**Tech Stack:** Python 3 标准库、Blender 5.0 `bpy``unittest`、现有 headless Blender runner。
设计依据:`docs/superpowers/specs/2026-07-23-quad-reducer-retopo-upgrade-design.md`
## Global Constraints
- 不加外部依赖。
- `collapse` 后端行为零变化;`--reducer` 默认仍为 `quad`QR 失败仍自动回退 collapse。
- 只 stage 本计划涉及的源码/文档;不碰用户脏工作区(Unity/src/`out_*` 产物)。
- 纯逻辑 TDDBlender 侧 `_remesh_quad`/`main` 改动由真机 smoke 覆盖,本地以 `py_compile`+回归把关。
## File Map
- Modify `Tools/ModelTranslator/qr_bridge.py``topology_acceptable``tol`;新增 `precollapse_target` 与 3 个常量。
- Modify `Tools/ModelTranslator/tests/test_qr_bridge.py`:软门 tol 与 `precollapse_target` 单测。
- Modify `Tools/ModelTranslator/bl_decimate.py`:新增 `_fill_small_holes``_remesh_quad` 补洞+软门;`main` 预 collapse+保四边。
- Modify `Tools/ModelTranslator/model_decimate.py`:加 `--qr-input-cap` 透传。
- Modify `Tools/ModelTranslator/tests/test_model_decimate_cli.py`:适配新增 argv 位。
- Modify `Tools/ModelTranslator/README.md`:记录升级后的 quad 行为与 `--qr-input-cap`
---
### Task 1: 纯策略(容忍软门 + 预 collapse 目标)
**Files:**
- Modify: `Tools/ModelTranslator/qr_bridge.py`
- Modify: `Tools/ModelTranslator/tests/test_qr_bridge.py`
- [ ] **Step 1: 写失败单测**
`tests/test_qr_bridge.py``if __name__ == "__main__":` 之前插入两个测试类:
```python
class TestTopologyTolerance(unittest.TestCase):
def test_tol_allows_small_increase(self):
# 封闭输入 + QR 多出 3 开边,容忍 12 内 -> 接受
self.assertTrue(qb.topology_acceptable(0, 0, 3, 0, tol=12))
def test_tol_rejects_large_increase(self):
self.assertFalse(qb.topology_acceptable(0, 0, 13, 0, tol=12))
def test_tol_applies_to_nonmanifold(self):
self.assertTrue(qb.topology_acceptable(0, 0, 0, 12, tol=12))
self.assertFalse(qb.topology_acceptable(0, 0, 0, 13, tol=12))
def test_default_tol_zero_strict(self):
# 默认 tol=0:现有严格语义不变
self.assertFalse(qb.topology_acceptable(0, 0, 1, 0))
self.assertTrue(qb.topology_acceptable(0, 0, 0, 0))
class TestPrecollapseTarget(unittest.TestCase):
def test_above_cap_returns_cap(self):
self.assertEqual(qb.precollapse_target(1000000, 50000), 50000)
def test_below_cap_returns_none(self):
self.assertIsNone(qb.precollapse_target(40000, 50000))
def test_at_cap_returns_none(self):
self.assertIsNone(qb.precollapse_target(50000, 50000))
def test_default_cap(self):
self.assertEqual(qb.precollapse_target(60000), qb.QR_INPUT_CAP)
self.assertIsNone(qb.precollapse_target(40000))
```
- [ ] **Step 2: 跑测试确认 RED**
Run(在 `Tools/ModelTranslator`):
```bash
python -m unittest tests.test_qr_bridge.TestTopologyTolerance tests.test_qr_bridge.TestPrecollapseTarget -v
```
Expected: `AttributeError``precollapse_target` 不存在)/ `TypeError``topology_acceptable` 不接受 `tol`)。
- [ ] **Step 3: 实现常量 + 函数**
`qr_bridge.py` 顶部常量 `QR_TIMEOUT = 120.0` 之后新增三个常量:
```python
QR_INPUT_CAP = 50000 # quad 后端 QR 前预 collapse 的中等密度目标面数
QR_HOLE_MAX_SIDES = 6 # 补洞上限边数:只补 <=6 边的薄部位小洞
QR_CATASTROPHIC_TOL = 12 # 软水密门容忍:补洞后仍多出超此数的开边/非流形才判灾难性
```
`topology_acceptable` 整个函数替换为带 `tol` 版本:
```python
def topology_acceptable(src_boundary, src_nonmanifold,
out_boundary, out_nonmanifold, tol=0):
"""QR 输出拓扑是否可接受:不比输入多出超过 tol 的开边(孔洞)/非流形边。
tol=0(默认)为零容忍严格门;quad 后端补小洞后用 tol=QR_CATASTROPHIC_TOL 只挡灾难性破损。"""
return (out_boundary <= src_boundary + tol
and out_nonmanifold <= src_nonmanifold + tol)
```
在文件末尾新增:
```python
def precollapse_target(face_count, cap=QR_INPUT_CAP):
"""QR 前预 collapse 目标:面数 > cap 返回 cap(先减到中等密度),否则 None(跳过)。
高密度网格直接 QR 必破洞,先 collapse 到中等密度给 QR 一个稳定输入。"""
return cap if face_count > cap else None
```
- [ ] **Step 4: 跑测试确认 GREEN + 全量回归**
Run
```bash
python -m unittest tests.test_qr_bridge -v
python -m unittest discover -s tests
```
Expected: 新测试全过;现有 `TestTopologyAcceptable`(4 参调用、tol 默认 0)仍全过;全量零失败。
- [ ] **Step 5: 提交**
```bash
git add -- Tools/ModelTranslator/qr_bridge.py Tools/ModelTranslator/tests/test_qr_bridge.py
git commit -m "ModelTranslator: QR tolerance soft-gate + precollapse target policy"
```
提交信息末尾附一行:`Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>`
---
### Task 2: bl_decimate 预 collapse + 补洞 + 软门 + 保四边
**Files:**
- Modify: `Tools/ModelTranslator/bl_decimate.py`
> `_remesh_quad`/`main` 依赖 bpy,无法纯单测;本任务以 `py_compile` + 回归把关,真机 smoke 在 Task 4。
- [ ] **Step 1: 新增 `_fill_small_holes`**
`bl_decimate.py``_edge_defects` 函数之后插入:
```python
def _fill_small_holes(obj, max_sides, warnings):
"""补掉 <=max_sides 边的小洞(QR 薄部位零星孔洞);异常降级不中断。"""
import bpy
try:
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.fill_holes(sides=max_sides)
bpy.ops.object.mode_set(mode='OBJECT')
except RuntimeError as e:
warnings.append("fill_holes 失败(%s),跳过补洞" % e)
```
- [ ] **Step 2: `_remesh_quad` 导回后补洞 + 软门**
`_remesh_quad` 中,找到这段(导回后的水密门块):
```python
retopo = new[0]
# 水密门:QR 引擎非确定性地会打出孔洞/非流形,破损即弃用 QR 回退 collapse
qr_boundary, qr_nonman = _edge_defects(retopo)
if not qr_bridge.topology_acceptable(src_boundary, src_nonman,
qr_boundary, qr_nonman):
warnings.append("QR 输出拓扑破损(开边 %d%d,非流形 %d%d),回退 collapse"
% (src_boundary, qr_boundary, src_nonman, qr_nonman))
```
整体替换为:
```python
retopo = new[0]
# 补小洞:QR 常在薄部位留零星小洞,先补掉再判软门
_fill_small_holes(retopo, qr_bridge.QR_HOLE_MAX_SIDES, warnings)
# 软水密门:补洞后仍比输入多出超容忍的破损才判灾难性,弃用 QR 回退 collapse
qr_boundary, qr_nonman = _edge_defects(retopo)
if not qr_bridge.topology_acceptable(src_boundary, src_nonman,
qr_boundary, qr_nonman,
tol=qr_bridge.QR_CATASTROPHIC_TOL):
warnings.append("QR 输出灾难性破损(开边 %d%d,非流形 %d%d),回退 collapse"
% (src_boundary, qr_boundary, src_nonman, qr_nonman))
```
(其后的 `bpy.data.objects.remove(retopo...)` / 恢复选中 / return False 与接受分支保持不变。)
- [ ] **Step 3: `main()` 解析 qr_input_cap**
`main()` 中,找到:
```python
reducer_arg = argv[5] if len(argv) > 5 else "collapse"
warnings = []
```
改为(其间新增一行):
```python
reducer_arg = argv[5] if len(argv) > 5 else "collapse"
qr_input_cap = int(argv[6]) if len(argv) > 6 else qr_bridge.QR_INPUT_CAP
warnings = []
```
- [ ] **Step 4: `main()` quad 分支预 collapse + 保四边**
找到 quad 分支:
```python
if reducer_arg == "quad":
if _remesh_quad(obj, target, warnings):
obj = bpy.context.view_layer.objects.active # QR 导回的 quad 对象
_triangulate(obj) # 三角化供导出/计数
cur = len(obj.data.polygons)
used_reducer = "quad"
did_quad = True
else:
used_reducer = "quad->collapse"
```
整体替换为:
```python
if reducer_arg == "quad":
# 预 collapse 到中等密度:给 QR 一个不炸洞的输入(高密度直接 QR 必破)
pre = qr_bridge.precollapse_target(len(obj.data.polygons), qr_input_cap)
if pre is not None:
_decimate(obj, decimate_ratio(pre, len(obj.data.polygons)))
_triangulate(obj)
if _remesh_quad(obj, target, warnings):
obj = bpy.context.view_layer.objects.active # QR 导回的 quad 对象
# 保四边:不三角化,直接导出四边网格(FBX/烘焙/Unity 均支持四边)
cur = len(obj.data.polygons)
used_reducer = "quad"
did_quad = True
else:
used_reducer = "quad->collapse"
```
`orig` 仍在此分支之前捕获 = 预 collapse 前的高模面数,作 `tris_before`。)
- [ ] **Step 5: 更新文件头 argv 文档**
找到 `bl_decimate.py` 顶部 docstring 里的调用行:
```
<src.fbx> <out.fbx> <target_tris> [unwrap:seam|smart] [overlap_tol] [reducer:collapse|quad]
```
改为:
```
<src.fbx> <out.fbx> <target_tris> [unwrap:seam|smart] [overlap_tol] [reducer:collapse|quad] [qr_input_cap]
```
- [ ] **Step 6: 编译 + 回归**
Run(在 `Tools/ModelTranslator`):
```bash
python -m py_compile qr_bridge.py bl_decimate.py
python -m unittest discover -s tests
```
Expected: 编译 exit 0;全部测试通过(本任务未改纯函数与 CLI 测试,回归应全绿)。
- [ ] **Step 7: 提交**
```bash
git add -- Tools/ModelTranslator/bl_decimate.py
git commit -m "ModelTranslator: quad backend precollapse + fill-holes + keep-quads"
```
提交信息末尾附一行:`Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>`
---
### Task 3: model_decimate 加 `--qr-input-cap`(并修 CLI 测试)
**Files:**
- Modify: `Tools/ModelTranslator/model_decimate.py`
- Modify: `Tools/ModelTranslator/tests/test_model_decimate_cli.py`
- [ ] **Step 1: 先改 CLI 测试为 RED(反映新增 argv 位)**
现有 `tests/test_model_decimate_cli.py` 断言 `args[-1] == "quad"`;新增 `--qr-input-cap` 后 reducer 不再是末位。把该文件里的断言块:
```python
args = run.call_args.args[2]
self.assertEqual(args[-1], "quad")
```
替换为:
```python
args = run.call_args.args[2]
# argv 顺序:input, out, tris, unwrap, overlap, reducer, qr_input_cap
self.assertEqual(args[5], "quad") # reducer 默认 quad
self.assertEqual(args[6], "50000") # qr_input_cap 默认 50000
```
- [ ] **Step 2: 跑测试确认 RED**
Run
```bash
python -m unittest tests.test_model_decimate_cli -v
```
Expected: FAIL(当前 `model_decimate` 只传 6 个 argv`args[6]` 越界 `IndexError`)。
- [ ] **Step 3: 加 `--qr-input-cap` 并透传**
`model_decimate.py` 顶部 import 处:
```python
from mt_run import find_blender, run_blender_script
```
其后新增:
```python
from qr_bridge import QR_INPUT_CAP
```
`--reducer` 参数定义之后新增:
```python
ap.add_argument("--qr-input-cap", type=int, default=QR_INPUT_CAP,
help="quad 后端 QR 前预 collapse 的中等密度目标面数(默认 50000);高密度模型直接 QR 会破洞")
```
`args = ap.parse_args()` 后的校验区新增:
```python
if args.qr_input_cap <= 0:
ap.error("--qr-input-cap 必须为正整数")
```
`run_blender_script` 调用:
```python
s = run_blender_script(blender, "bl_decimate.py",
[args.input, out_fbx, str(args.tris), args.unwrap,
"" if args.max_overlap is None else str(args.max_overlap),
args.reducer])
```
改为(追加 `str(args.qr_input_cap)`):
```python
s = run_blender_script(blender, "bl_decimate.py",
[args.input, out_fbx, str(args.tris), args.unwrap,
"" if args.max_overlap is None else str(args.max_overlap),
args.reducer, str(args.qr_input_cap)])
```
- [ ] **Step 4: 跑测试确认 GREEN + 编译 + --help**
Run
```bash
python -m py_compile model_decimate.py
python -m unittest tests.test_model_decimate_cli -v
python model_decimate.py --help
```
Expected: 编译 exit 0CLI 测试通过;`--help``--qr-input-cap`
- [ ] **Step 5: 提交**
```bash
git add -- Tools/ModelTranslator/model_decimate.py Tools/ModelTranslator/tests/test_model_decimate_cli.py
git commit -m "ModelTranslator: expose --qr-input-cap, adapt CLI test to new argv"
```
提交信息末尾附一行:`Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>`
---
### Task 4: 真机验证与文档
**Files:**
- Modify: `Tools/ModelTranslator/README.md`
Blender 路径:`D:/tools/Blender/blender-5.0.0-windows-x64/blender.exe`(用 `--blender` 显式传)。
- [ ] **Step 1: quad 后端真机 smoke(有机角色,验证保四边)**
Run`out_*` 已被 gitignore,勿提交产物):
```bash
python model_decimate.py "C:/Users/Administrator/Downloads/supergirl.fbx" --tris 10000 --reducer quad -o out_sg_test --blender "D:/tools/Blender/blender-5.0.0-windows-x64/blender.exe"
```
Expected:打印 `后端: quad``out_sg_test/supergirl_low.fbx` 存在。用下面命令验证输出以四边为主、缺陷少:
```bash
"D:/tools/Blender/blender-5.0.0-windows-x64/blender.exe" -b --factory-startup --python-expr "
import bpy,bmesh
bpy.ops.wm.read_factory_settings(use_empty=True)
bpy.ops.import_scene.fbx(filepath=r'D:/UD/AI/AIC#Project/Tools/ModelTranslator/out_sg_test/supergirl_low.fbx')
o=[x for x in bpy.data.objects if x.type=='MESH'][0]
bm=bmesh.new();bm.from_mesh(o.data)
q=sum(1 for f in bm.faces if len(f.verts)==4);t=sum(1 for f in bm.faces if len(f.verts)==3)
b=sum(1 for e in bm.edges if len(e.link_faces)==1);nm=sum(1 for e in bm.edges if len(e.link_faces)>2)
print('CHECK faces=%d quad=%d tri=%d boundary=%d nonmanifold=%d'%(len(bm.faces),q,t,b,nm))
"
```
Expected`quad` 数 >> `tri` 数(以四边为主);`boundary`/`nonmanifold` 为个位数小值(补洞 + 软门生效)。
- [ ] **Step 2: collapse 后端回归(零变化)**
Run
```bash
python model_decimate.py "C:/Users/Administrator/Downloads/supergirl.fbx" --tris 10000 --reducer collapse -o out_sg_collapse --blender "D:/tools/Blender/blender-5.0.0-windows-x64/blender.exe"
```
Expected:打印 `后端: collapse`;面数正好 10000(±3%);输出全三角(collapse 行为与升级前一致)。
- [ ] **Step 3: 强制回退验证**
Run
```bash
QR_ENGINE="/nonexistent/xremesh.exe" python model_decimate.py "C:/Users/Administrator/Downloads/supergirl.fbx" --tris 10000 --reducer quad -o out_sg_fb --blender "D:/tools/Blender/blender-5.0.0-windows-x64/blender.exe"
```
Expected:打印 `后端: quad->collapse`;输出正常生成;warnings 含"未找到 QuadRemesher 引擎"。
- [ ] **Step 4: 更新 README**
`Tools/ModelTranslator/README.md``--reducer` 段落,把 quad 的回退描述扩充为升级后行为。找到该 bullet 中的这句:
```markdown
**水密门**:QR 引擎在激进减面下会*非确定性*地打出孔洞/非流形边(同一封闭输入实测约 85% 次数破碎),故导回后比对输入拓扑——QR 若比输入新增开边(孔洞)/非流形边即判破损、弃用 QR 回退 collapse(保证减面输出的拓扑完整性不劣于源模,杜绝"破碎面")。
```
替换为:
```markdown
**升级流程**:quad 后端先把高密度输入**预 collapse 到中等密度**`--qr-input-cap`,默认 50000——高密度网格直接 QR 必破洞),再 QR,导回后 `fill_holes` 补掉薄部位小洞,最后**软水密门**(补洞后仍比输入多出超容忍的破损才判灾难性、回退 collapse)。quad 成功时**保留四边、不三角化**导出(FBX/烘焙/Unity 均支持四边;这修掉了旧版 quad 被三角化的问题)。
```
- [ ] **Step 5: 最终自动化验证**
Run
```bash
python -m py_compile qr_bridge.py bl_decimate.py model_decimate.py
python -m unittest discover -s tests
```
Expected:编译 exit 0;全部测试通过、零失败。
- [ ] **Step 6: 检查 diff 与暂存范围**
Run
```bash
git status --short
git diff --check
```
Expected:无空白错误;`out_*` 产物未被 stage;用户既有脏文件(unity_assets.py 等)保持未暂存。
- [ ] **Step 7: 提交文档**
```bash
git add -- Tools/ModelTranslator/README.md docs/superpowers/specs/2026-07-23-quad-reducer-retopo-upgrade-design.md docs/superpowers/plans/2026-07-23-quad-reducer-retopo-upgrade.md
git commit -m "ModelTranslator: document quad retopo upgrade"
```
提交信息末尾附一行:`Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>`
---
## Self-Review 记录
- **Spec 覆盖**:预 collapseTask2 Step4 + Task1 `precollapse_target`)、补小洞(Task2 Step1/2)、软门(Task1 tol + Task2 Step2)、保四边不三角化(Task2 Step4 去掉 `_triangulate`)、`--qr-input-cap`(Task3)、水密门保底/回退(Task2 Step2 软门 + 现有回退)、collapse 零变化(未动 collapse 分支)、测试(Task1 单测 + Task3 CLI 测试 + Task4 真机)、READMETask4)——全覆盖。
- **占位符**:无 TBD/TODO;所有代码步骤含完整代码与锚点。
- **类型一致**`topology_acceptable(...tol=0)` 定义(Task1)与 `_remesh_quad` 调用 `tol=qr_bridge.QR_CATASTROPHIC_TOL`Task2)一致;`precollapse_target` 定义(Task1)与 main 调用(Task2)签名一致;argv 第 7 位 `qr_input_cap` 在 bl_decimateTask2 Step3 `argv[6]`)、model_decimateTask3 追加第 7 位)、CLI 测试(Task3 `args[6]`)三处口径一致;常量 `QR_INPUT_CAP`/`QR_HOLE_MAX_SIDES`/`QR_CATASTROPHIC_TOL` 定义(Task1)与消费(Task2/Task3)一致。