From 25f84f8557d903558b01ee20bf281c0d2e97363a Mon Sep 17 00:00:00 2001 From: ud18010 Date: Thu, 23 Jul 2026 15:09:24 +0800 Subject: [PATCH] ModelTranslator: document quad retopo upgrade Co-Authored-By: Claude Opus 4.8 --- Tools/ModelTranslator/README.md | 2 +- .../2026-07-23-quad-reducer-retopo-upgrade.md | 433 ++++++++++++++++++ ...7-23-quad-reducer-retopo-upgrade-design.md | 126 +++++ 3 files changed, 560 insertions(+), 1 deletion(-) create mode 100644 docs/superpowers/plans/2026-07-23-quad-reducer-retopo-upgrade.md create mode 100644 docs/superpowers/specs/2026-07-23-quad-reducer-retopo-upgrade-design.md diff --git a/Tools/ModelTranslator/README.md b/Tools/ModelTranslator/README.md index aaecb58c..fa312446 100644 --- a/Tools/ModelTranslator/README.md +++ b/Tools/ModelTranslator/README.md @@ -47,7 +47,7 @@ python model_translator.py out_bake/well1500/well1500.fbx # -> out/well1500/ ``` - **model_decimate.py**:多 mesh 自动 join;**减面前清理网格**(按局部包围盒对角线相对焊接重合点 + 去退化面,给后端更干净的流形、减少被迫加的 seam);按 `--reducer` 后端减到 `--tris`(默认 Quad Remesher,失败自动回退 Decimate collapse;collapse 后端为 ±3%,最多 2 轮修正);旧 UV 全删后重展——默认锐边 seam 整岛展开(SLIM 展开后 `minimize_stretch` 松弛 + `average_islands_scale` 纹素均衡),质量门不达标(翻转 >2% 或重叠 >8%)先按更低 seam 角度扫描(55/45/35°)取过门且岛数最少者抢救,全失败才回退 Smart UV Project,`--unwrap smart` 可直接选投影式展开;**`--max-overlap 0.15` 放宽重叠门限**,让更高 seam 角度(岛更少)能过门被选中——岛数优先、可接受略高重叠时用(翻转门限始终严格);排布默认用 UVPackmaster 4 重排(只在最终选定 UV 上跑一次,旋转步进调细到 15° 以利薄斜条对齐提升利用率;UVPM 启发式搜索在 headless 下会随机崩溃引擎故未启用),UVPM4 不可用自动沿用内置 pack_islands 布局;输出 `<名>_low.fbx`(`-o` 改目录)与 `<名>_low_uv.png`(UV 线框观察图,便于人工查阅切分/排布/碎岛——Blender 抽 UV 几何,系统 Python 用 Pillow 绘制,因 headless 无 GPU 无法用 Blender 直接出 PNG),日志报 UV 岛数/利用率与实际生效的 seam 角度 -- **`--reducer collapse|quad`**(默认 `quad`):减面后端。`quad` 用 **Quad Remesher** 出规整四边拓扑(针对 collapse 的碎三角/UV 展开难痛点),headless 下**直接调 `Engine/xremesh.exe` 引擎子进程**(导出选中 mesh → 写 RetopoSettings.txt → 阻塞轮询 progress.txt → 导回 retopo.fbx),**绕开插件的 modal 操作符**(modal 在 `blender -b` 后台不执行,直接调操作符必然失败)。面数按 `--tris ÷ 2` 换算为目标四边形数,用 `ExactQuadCount=1` 尊重目标数(自适应模式在高细节硬表面模型上会把面数炸开数倍并连带 UV 重叠爆炸,故禁用);输出为近似面数(约 ±20%)。**任何失败——引擎缺失/超时/引擎报错/无有效输出/导回失败——都记警告并自动回退 collapse**,摘要 `reducer` 字段标 `quad->collapse`,绝不中断。**水密门**:QR 引擎在激进减面下会*非确定性*地打出孔洞/非流形边(同一封闭输入实测约 85% 次数破碎),故导回后比对输入拓扑——QR 若比输入新增开边(孔洞)/非流形边即判破损、弃用 QR 回退 collapse(保证减面输出的拓扑完整性不劣于源模,杜绝"破碎面")。`QR_ENGINE` 环境变量可覆盖引擎路径(**权威**:显式设了就以它为准,无效即视同缺失走回退)。因此 quad 只在 QR 确实产出干净水密网格时才真正生效,否则安全回退 collapse;QR 更适合有机/密网资产而非激进减面 +- **`--reducer collapse|quad`**(默认 `quad`):减面后端。`quad` 用 **Quad Remesher** 出规整四边拓扑(针对 collapse 的碎三角/UV 展开难痛点),headless 下**直接调 `Engine/xremesh.exe` 引擎子进程**(导出选中 mesh → 写 RetopoSettings.txt → 阻塞轮询 progress.txt → 导回 retopo.fbx),**绕开插件的 modal 操作符**(modal 在 `blender -b` 后台不执行,直接调操作符必然失败)。面数按 `--tris ÷ 2` 换算为目标四边形数,用 `ExactQuadCount=1` 尊重目标数(自适应模式在高细节硬表面模型上会把面数炸开数倍并连带 UV 重叠爆炸,故禁用);输出为近似面数(约 ±20%)。**任何失败——引擎缺失/超时/引擎报错/无有效输出/导回失败——都记警告并自动回退 collapse**,摘要 `reducer` 字段标 `quad->collapse`,绝不中断。**升级流程**:quad 后端先把高密度输入**预 collapse 到中等密度**(`--qr-input-cap`,默认 50000——高密度网格直接 QR 必破洞),再 QR,导回后 `fill_holes` 补掉薄部位小洞,最后**软水密门**(补洞后仍比输入多出超容忍的破损才判灾难性、回退 collapse)。quad 成功时**保留四边、不三角化**导出(FBX/烘焙/Unity 均支持四边;这修掉了旧版 quad 被三角化的问题)。`QR_ENGINE` 环境变量可覆盖引擎路径(**权威**:显式设了就以它为准,无效即视同缺失走回退)。QR 输出为近似面数(约 ±20%,四边口径 `--tris÷2`);实测有机角色可保四边(如 100 万面 supergirl → 约 5000 四边、开边≤3),硬表面直边仍会被 QR 波动化(那类模型用 collapse) - **model_bake.py**:Cycles selected-to-active 烘焙。normal(切线空间 OpenGL +Y)/ao 直接烘;color/metallic/roughness 把高模 Principled 对应输入接 Emission 烘 EMIT。`--size` 默认 2048,`--samples` AO 采样默认 64,射线距离默认低模包围盒对角线 2%(`--ray-distance` 覆盖);低模无 UV 会报错,高低模包围盒差超 10% 打警告;导入后自动应用物体变换——未应用缩放的 FBX(如 cm 单位导出的 scale=0.01)会把射线距离缩到近零导致大面积烘空(脏色);固定 `-t 1` 单线程跑 Blender——5.0 的 selected-to-active 射线求交多线程有竞争,会随机 EXCEPTION_ACCESS_VIOLATION 崩溃 - 输出命名 `<名>_color/normal/metallic/roughness/ao.png`,正好命中转换器的纯网格贴图命名约定 diff --git a/docs/superpowers/plans/2026-07-23-quad-reducer-retopo-upgrade.md b/docs/superpowers/plans/2026-07-23-quad-reducer-retopo-upgrade.md new file mode 100644 index 00000000..fdc1d2a3 --- /dev/null +++ b/docs/superpowers/plans/2026-07-23-quad-reducer-retopo-upgrade.md @@ -0,0 +1,433 @@ +# `--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` 可 TDD;Blender 操作(预 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_*` 产物)。 +- 纯逻辑 TDD;Blender 侧 `_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 ` + +--- + +### 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 里的调用行: +``` + [unwrap:seam|smart] [overlap_tol] [reducer:collapse|quad] +``` +改为: +``` + [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 ` + +--- + +### 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 0;CLI 测试通过;`--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 ` + +--- + +### 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 ` + +--- + +## Self-Review 记录 + +- **Spec 覆盖**:预 collapse(Task2 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 真机)、README(Task4)——全覆盖。 +- **占位符**:无 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_decimate(Task2 Step3 `argv[6]`)、model_decimate(Task3 追加第 7 位)、CLI 测试(Task3 `args[6]`)三处口径一致;常量 `QR_INPUT_CAP`/`QR_HOLE_MAX_SIDES`/`QR_CATASTROPHIC_TOL` 定义(Task1)与消费(Task2/Task3)一致。 diff --git a/docs/superpowers/specs/2026-07-23-quad-reducer-retopo-upgrade-design.md b/docs/superpowers/specs/2026-07-23-quad-reducer-retopo-upgrade-design.md new file mode 100644 index 00000000..8722bfd3 --- /dev/null +++ b/docs/superpowers/specs/2026-07-23-quad-reducer-retopo-upgrade-design.md @@ -0,0 +1,126 @@ +# `--reducer quad` 升级为可靠四边重拓扑 设计 + +日期:2026-07-23 + +## 背景与动机 + +真机排查(gargoyle 500k、supergirl 1M)确认了现有 `--reducer quad` 的三个问题: + +1. **高密度输入必破洞**:QR 引擎从很高密度网格(500k~1M 面)激进减面时,非确定性地打出 + 孔洞/非流形边,水密门(零容忍)几乎总判破损 → 回退 collapse。实测把高模先 collapse 到 + **中等密度(~50k)** 再喂 QR,输出即干净。 +2. **导出前被三角化**:`bl_decimate.main` 在 quad 成功路径也调 `_triangulate`,导致"quad 后端" + 的 FBX 里其实是三角面,四边拓扑被毁——与"要四边"矛盾。 +3. **零容忍门太严**:有机角色(supergirl)QR 常只留 1 个薄部位小洞(约 3 开边),门把这个 + 近乎干净的 QR 一票否决。补掉小洞即净。 + +用户决定:**改造现有 `--reducer quad`**,把"预 collapse + QR + 补小洞 + 保四边"整套配方内建, +让 quad 后端真正可用。`collapse` 后端与默认值(quad)不变。 + +## 约束 + +- 不加外部依赖。 +- `collapse` 后端行为**零变化**;`--reducer` 默认仍为 `quad`。 +- QR 任何灾难性失败仍**自动回退 collapse**,绝不中断(保留现有回退契约)。 +- 不 stage 用户脏工作区(Unity/src/产物)。 +- 纯策略(cap 判断、软门)与 Blender 操作分离,纯逻辑 TDD。 + +## 新流程(`--reducer quad`) + +`bl_decimate.main()` 的 quad 分支改为: + +1. **预 collapse**:`_clean_mesh` + `_triangulate` 后,若面数 > `qr_input_cap`(默认 50000), + 先 `_decimate` 到 cap + `_triangulate`;≤ cap 则跳过。给 QR 一个不炸洞的中等密度输入。 +2. **QR 重拓扑**:`_remesh_quad` 导出预处理网格 → `xremesh.exe`(`ExactQuadCount=1`, + 目标 `tris_to_target_quads(--tris)` 四边)→ 导回。 +3. **补小洞**:QR 导回后 `fill_holes(sides <= QR_HOLE_MAX_SIDES=6)`,补掉薄部位零星小洞。 +4. **软水密门**:补洞后比对输入基线——`topology_acceptable(src_b, src_nm, out_b, out_nm, + tol=QR_CATASTROPHIC_TOL)`;`out_b <= src_b + tol and out_nm <= src_nm + tol` 则接受 + (保留四边);否则判灾难性破碎,弃用 QR **回退 collapse**(标 `quad->collapse`)。 +5. **保四边导出**:quad 成功路径**不再 `_triangulate`**,`_do_unwrap` 与导出直接作用于四边网格 + (FBX/Cycles 烘焙/Unity 均支持四边;Unity 导入自动三角化)。 + +collapse 路径与 quad->collapse 回退路径**仍三角化**(collapse 本就产三角)。 + +## 架构与接口 + +### `qr_bridge.py`(纯逻辑,不 import bpy) + +- 常量:`QR_INPUT_CAP = 50000`、`QR_HOLE_MAX_SIDES = 6`、`QR_CATASTROPHIC_TOL = 12`。 +- 修改 `topology_acceptable(src_boundary, src_nonmanifold, out_boundary, out_nonmanifold, + tol=0)`:加 `tol` 参数(默认 0,向后兼容现有调用/测试),判定改为 + `out_boundary <= src_boundary + tol and out_nonmanifold <= src_nonmanifold + tol`。 +- 新增 `precollapse_target(face_count, cap=QR_INPUT_CAP) -> int | None`:`face_count > cap` + 返回 `cap`,否则 `None`(跳过预 collapse)。 + +### `bl_decimate.py`(Blender 内) + +- 新增 `_fill_small_holes(obj, max_sides)`:编辑模式全选 → `bpy.ops.mesh.fill_holes(sides=max_sides)` + → 回物体模式。异常降级不中断。 +- 修改 `_remesh_quad(obj, target_tris, warnings)`:导回 retopo 后 + (a) `_fill_small_holes(retopo, qr_bridge.QR_HOLE_MAX_SIDES)`; + (b) 用软门 `topology_acceptable(..., tol=QR_CATASTROPHIC_TOL)` 判定; + (c) 接受则删原 obj、retopo 设为 active、return True(**不三角化**);拒绝则弃 retopo、 + 恢复 obj 选中态、return False。其余(引擎发现/子进程/轮询/选择态恢复)不变。 +- 修改 `main()`: + - 解析 `qr_input_cap = int(argv[6]) if len(argv) > 6 else QR_INPUT_CAP`。 + - quad 分支:`_remesh_quad` 前按 `precollapse_target` 预 collapse;成功后**去掉** + `_triangulate(obj)`(保四边)。`orig`(tris_before)仍取预 collapse 前的高模面数。 + +### `model_decimate.py`(CLI) + +- 加 `--qr-input-cap`(type int,default `QR_INPUT_CAP`),透传为 Blender argv 第 7 位(index 6)。 +- 校验 `> 0`。摘要打印沿用(`后端` 已有)。 + +## 数据流 + +``` +FBX --import--> join --clean--> triangulate --orig + reducer=quad: + 面数 > cap? --yes--> collapse 到 cap --triangulate + _remesh_quad: export --xremesh(exact)--> retopo --import + fill_holes(<=6) --> 软门(tol) 过? --yes--> 保留四边(不三角化) + --no--> 弃用, 回退 collapse(三角) + reducer=collapse 或 quad->collapse: decimate 循环(三角) +--> _do_unwrap(smart/seam) --> export(四边或三角) + MT_SUMMARY{reducer,...} +``` + +## 错误处理 + +| 情况 | 处理 | +|---|---| +| 引擎缺失/超时/报错/无输出/导回无 mesh | warning,回退 collapse(现有) | +| fill_holes 异常 | warning,跳过补洞,继续走软门 | +| 补洞后仍灾难性破损(超 tol) | warning,弃 QR 回退 collapse | +| 预 collapse 后 QR 仍失败 | 回退 collapse(在已预collapse的网格上继续减到目标) | + +回退后 `reducer` 标 `quad->collapse`,绝不中断。 + +## 已知取舍 + +- 预 collapse 会在 QR 前损失部分细节 → 低模形状比"直接 QR"略有偏差(实测形变从 ~0.015% + 升到 ~0.06%,量级仍很小)。**烘焙细节来自原始高模**(model_bake 用 high FBX),故低模这点 + 偏差对最终贴图影响小;换来的是 QR 不破洞、能保四边。取舍值得。 +- 补洞产生的少量 n-gon 补丁(≤6 边)与 QR 偶发三角使输出"以四边为主",非 100% 纯四边。 + +## 测试 + +- **`tests/test_qr_bridge.py`**: + - `topology_acceptable` 加 `tol`:`(0,0,3,0,tol=12)->True`、`(0,0,13,0,tol=12)->False`、 + 默认 `tol=0` 时 `(0,0,1,0)->False`(现有语义不变)。 + - `precollapse_target`:`(1000000,50000)->50000`、`(40000,50000)->None`、边界 `(50000,50000)->None`。 +- **回归**:`python -m unittest discover -s tests` 全绿(现有测试含 `topology_acceptable` 旧调用 + 仍按 tol=0 通过)。 +- **真机 smoke**: + - `model_decimate supergirl.fbx --tris 10000 --reducer quad`:backend=`quad`、输出以四边为主 + (quad 数 >> tri)、开边/非流形 ≤ 输入基线+tol、有 UV。 + - `gargoyle.fbx --tris 10000 --reducer quad`:同上或合理 `quad->collapse`。 + - `--reducer collapse` 回归:面数、拓扑与升级前一致(零变化)。 + - 强制回退(`QR_ENGINE` 指错):`quad->collapse`、输出正常。 + +## 明确不做(YAGNI) + +- 不做硬边引导/直边保真(实测无效,另属课题)。 +- 不暴露 fill-holes sides、catastrophic tol 为 CLI(内置常量足够;只暴露 `--qr-input-cap`)。 +- 不改 model_bake/model_translator(四边低模它们已支持)。 +- 不追求 100% 纯四边(少量 n-gon/tri 可接受)。