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

19 KiB
Raw Blame History

--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_quadmainmodel_decimate.py--qr-input-cap 透传。collapse 后端与默认值零变化。

Tech Stack: Python 3 标准库、Blender 5.0 bpyunittest、现有 headless Blender runner。

设计依据:docs/superpowers/specs/2026-07-23-quad-reducer-retopo-upgrade-design.md

Global Constraints

  • 不加外部依赖。
  • collapse 后端行为零变化;--reducer 默认仍为 quadQR 失败仍自动回退 collapse。
  • 只 stage 本计划涉及的源码/文档;不碰用户脏工作区(Unity/src/out_* 产物)。
  • 纯逻辑 TDDBlender 侧 _remesh_quad/main 改动由真机 smoke 覆盖,本地以 py_compile+回归把关。

File Map

  • Modify Tools/ModelTranslator/qr_bridge.pytopology_acceptabletol;新增 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.pyif __name__ == "__main__": 之前插入两个测试类:

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):

python -m unittest tests.test_qr_bridge.TestTopologyTolerance tests.test_qr_bridge.TestPrecollapseTarget -v

Expected: AttributeErrorprecollapse_target 不存在)/ TypeErrortopology_acceptable 不接受 tol)。

  • Step 3: 实现常量 + 函数

qr_bridge.py 顶部常量 QR_TIMEOUT = 120.0 之后新增三个常量:

QR_INPUT_CAP = 50000        # quad 后端 QR 前预 collapse 的中等密度目标面数
QR_HOLE_MAX_SIDES = 6       # 补洞上限边数:只补 <=6 边的薄部位小洞
QR_CATASTROPHIC_TOL = 12    # 软水密门容忍:补洞后仍多出超此数的开边/非流形才判灾难性

topology_acceptable 整个函数替换为带 tol 版本:

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)

在文件末尾新增:

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

python -m unittest tests.test_qr_bridge -v
python -m unittest discover -s tests

Expected: 新测试全过;现有 TestTopologyAcceptable(4 参调用、tol 默认 0)仍全过;全量零失败。

  • Step 5: 提交
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 函数之后插入:

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 中,找到这段(导回后的水密门块):

        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))

整体替换为:

        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() 中,找到:

    reducer_arg = argv[5] if len(argv) > 5 else "collapse"
    warnings = []

改为(其间新增一行):

    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 分支:

    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"

整体替换为:

    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):

python -m py_compile qr_bridge.py bl_decimate.py
python -m unittest discover -s tests

Expected: 编译 exit 0;全部测试通过(本任务未改纯函数与 CLI 测试,回归应全绿)。

  • Step 7: 提交
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 不再是末位。把该文件里的断言块:

        args = run.call_args.args[2]
        self.assertEqual(args[-1], "quad")

替换为:

        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

python -m unittest tests.test_model_decimate_cli -v

Expected: FAIL(当前 model_decimate 只传 6 个 argvargs[6] 越界 IndexError)。

  • Step 3: 加 --qr-input-cap 并透传

model_decimate.py 顶部 import 处:

from mt_run import find_blender, run_blender_script

其后新增:

from qr_bridge import QR_INPUT_CAP

--reducer 参数定义之后新增:

    ap.add_argument("--qr-input-cap", type=int, default=QR_INPUT_CAP,
                    help="quad 后端 QR 前预 collapse 的中等密度目标面数(默认 50000);高密度模型直接 QR 会破洞")

args = ap.parse_args() 后的校验区新增:

    if args.qr_input_cap <= 0:
        ap.error("--qr-input-cap 必须为正整数")

run_blender_script 调用:

    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)):

    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

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: 提交
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(有机角色,验证保四边)

Runout_* 已被 gitignore,勿提交产物):

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:打印 后端: quadout_sg_test/supergirl_low.fbx 存在。用下面命令验证输出以四边为主、缺陷少:

"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))
"

Expectedquad 数 >> tri 数(以四边为主);boundary/nonmanifold 为个位数小值(补洞 + 软门生效)。

  • Step 2: collapse 后端回归(零变化)

Run

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

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 中的这句:

**水密门**:QR 引擎在激进减面下会*非确定性*地打出孔洞/非流形边(同一封闭输入实测约 85% 次数破碎),故导回后比对输入拓扑——QR 若比输入新增开边(孔洞)/非流形边即判破损、弃用 QR 回退 collapse(保证减面输出的拓扑完整性不劣于源模,杜绝"破碎面")。

替换为:

**升级流程**:quad 后端先把高密度输入**预 collapse 到中等密度**`--qr-input-cap`,默认 50000——高密度网格直接 QR 必破洞),再 QR,导回后 `fill_holes` 补掉薄部位小洞,最后**软水密门**(补洞后仍比输入多出超容忍的破损才判灾难性、回退 collapse)。quad 成功时**保留四边、不三角化**导出(FBX/烘焙/Unity 均支持四边;这修掉了旧版 quad 被三角化的问题)。
  • Step 5: 最终自动化验证

Run

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

git status --short
git diff --check

Expected:无空白错误;out_* 产物未被 stage;用户既有脏文件(unity_assets.py 等)保持未暂存。

  • Step 7: 提交文档
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_TOLTask2)一致;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)一致。