# ModelTranslator 减面可选后端 Quad Remesher 设计 日期:2026-07-22 ## 背景与动机 现有减面管线 `model_decimate.py` → `bl_decimate.py` 用 Blender 原生 **DECIMATE(COLLAPSE)** modifier 按 `--tris` 目标三角面数减面。用户不满的核心痛点: 1. **拓扑乱 / 碎三角**:COLLAPSE 产出狭长不规则三角,影响着色、变形、法线。 2. **UV 展开难**:碎三角导致 seam/UV 岛碎、利用率低。 用户已安装 **Quad Remesher(Exoside,重打包版)** Blender 插件,可产出规整四边拓扑, 从根子上改善以上两点。本设计将其作为**可选减面后端**接入,默认不改变现有行为。 ## 关键技术结论(已 spike 验证) `bpy.ops.qremesher.remesh()` 是 **modal 操作符**(`modal_handler_add` + timer,返回 `RUNNING_MODAL`)。`blender -b` 后台无事件循环,modal 永不触发,结果 FBX 永不导回—— **直接调操作符在 headless 必然失败**(与 UVPM headless 崩的教训同类)。 真正的重活是**解耦的外部引擎进程**:插件只是"桥",做导出 FBX → 写 settings → 跑 `Engine/xremesh.exe -s settings.txt` 子进程 → 轮询 `progress.txt` → 导回 retopo。 本设计**自己重实现这 ~40 行桥胶水**,完全确定、后台安全、不依赖 modal、不受 `--factory-startup` 影响、不需 enable 插件。 **Spike 实测**:icosphere 5120 tris → `TargetQuadCount=500` → 0.7s 出 441 面全 quad, exit 0、progress=2;重打包版无许可问题能直接跑。 ## 约束 - 不加外部依赖(纯标准库 + bpy)。 - 不覆盖输入 FBX;只在生成的 `_low.fbx` 输出重拓扑数据。 - 现有 CLI/输出目录/命名/`collapse` 路径行为**零变化**(`--reducer` 默认 `collapse`)。 - QR 失败(引擎缺失/超时/负值/导回空)一律记 warning 并**回退 collapse,绝不中断**—— 与现有 UVPM"失败即回退"同哲学。 - 面数口径:`target_quads = tris // 2`,接受 QR 近似输出(约 ±20%),不做二次 collapse (二次 collapse 会重新引入碎三角,抵消 QR 好处)。**用 `ExactQuadCount=1` 尊重目标数** ——真机验证发现自适应模式(`ExactQuadCount=0`)在高细节硬表面模型上面数暴涨数倍 (well1500 目标 2500→11500 quads)并连带 seam 展开重叠爆炸,故禁用自适应。 - `QR_ENGINE` 覆盖为**权威**语义:显式设了就以它为准,无效路径即视同缺失走 collapse 回退 (不再静默自动发现,保证回退契约可测)。 ## 架构 分三层,纯逻辑与 Blender 操作分离,便于 TDD: ### 1. 新增纯模块 `qr_bridge.py`(不 import bpy) | 函数 | 职责 | |---|---| | `find_qr_engine() -> str \| None` | 按标准 addon 路径查 `xremesh.exe`,找不到返回 None | | `tris_to_target_quads(tris) -> int` | `max(1, tris // 2)`;1 quad ≈ 2 tris | | `build_settings(in_fbx, out_fbx, prog, target_quads, *, adaptive=50, exact=False, hard_edges=True) -> str` | 生成 `RetopoSettings.txt` 文本(HostApp/FileIn/FileOut/ProgressFile/TargetQuadCount/CurvatureAdaptivness/ExactQuadCount/AutoDetectHardEdges 等) | | `parse_progress(text) -> (state, msg)` | `state ∈ {"running","success","error"}`;首行 `2`=success,`<0`=error(次行为文本),空/其它=running | `find_qr_engine` 搜索顺序:`QR_ENGINE` 环境变量 → `%APPDATA%/Blender Foundation/Blender/*/scripts/addons/QuadRemesher/Engine/xremesh.exe`。 ### 2. `bl_decimate.py` 新增 `_remesh_quad(obj, target_tris, warnings) -> bool` Blender 内执行,步骤: 1. `find_qr_engine()`;None → warning,return False。 2. 建工作目录(`tempfile.gettempdir()/mt_qr/`),清旧 `retopo.fbx`/`progress.txt`。 3. 选中 `obj`,`bpy.ops.export_scene.fbx(use_selection=True)` 导出 `inputMesh.fbx`。 4. `build_settings(...)` 写 `RetopoSettings.txt`(`hard_edges=True`,`exact=False`)。 5. `subprocess.Popen([engine, "-s", settings])`,阻塞轮询 `progress.txt`(间隔 0.3s, 超时默认 **120s**): - `parse_progress` → `success` 且 `retopo.fbx` 非空 → 进入 6; - `error` / 超时 / 进程退出但无输出 → warning,return False。 6. 记录旧对象集合 → `bpy.ops.import_scene.fbx(retopo.fbx)` → 删除原 `obj`,把导回的 mesh 设为新 active object(供后续展 UV/导出复用)。return True。 **任何异常都被捕获 → warning → return False**,不抛出。 ### 3. `main()` 分流 - 解析第 5 个 argv `reducer`(`"collapse"`(默认)/`"quad"`)。 - 流程: - `_clean_mesh(obj)`(现有) - `reducer == "quad"` 且 `_remesh_quad()` 成功 → **跳过 DECIMATE**,记 `reducer="quad"`; 否则(或 QR 失败回退)走现有 `_triangulate` + collapse 循环,记 `reducer="collapse"` 或 `"quad->collapse"`。 - 之后照旧:`_do_unwrap`(在更干净的拓扑上展 UV)→ 三角化保证导出为三角 → 导出。 - `MT_SUMMARY` 加 `reducer` 字段。 > 注:QR 输出为 quad,展 UV 在 quad 上进行(seam 更干净);导出前需确保三角化 > (复用现有 `_triangulate`),使 `_low.fbx` 仍为三角网格,兼容下游烘焙。 ### 4. `model_decimate.py` CLI - 加 `--reducer {collapse,quad}`,`default="collapse"`,透传给 Blender argv 第 5 位。 - 打印实际后端与面数:`后端=quad 面数 20000 -> 882`(或 `quad->collapse` 回退提示)。 ## 数据流 ``` FBX --import--> join --clean--> [reducer 分流] quad: export inputMesh.fbx --xremesh.exe--> retopo.fbx --import--> 全 quad obj collapse: DECIMATE(ratio) 循环 --> _do_unwrap(seam+UVPM) --triangulate--> export _low.fbx + MT_SUMMARY{reducer,...} ``` ## 错误处理 | 情况 | 处理 | |---|---| | 找不到 xremesh.exe | warning,回退 collapse | | 引擎超时(>120s) | kill 进程,warning,回退 collapse | | progress 负值(含无许可/EULA 的 -2) | warning(带引擎文本),回退 collapse | | 进程退出但 retopo.fbx 空/缺 | warning,回退 collapse | | 导回 FBX 无 mesh | warning,回退 collapse | 回退后 `reducer` 字段标 `quad->collapse`,用户可从摘要看出实际走了哪条路。 ## 测试 - **`tests/test_qr_bridge.py`**(纯函数,TDD): - `tris_to_target_quads`:偶/奇/极小值(`1→1`, `10000→5000`, `1→1`)。 - `build_settings`:含关键行、路径转义、`exact`/`hard_edges` 开关映射正确。 - `parse_progress`:`"2"`→success、`"-3\n失败"`→error 带文本、`""`/`"0.5"`→running。 - `find_qr_engine`:环境变量优先、缺失返回 None(用临时目录构造)。 - **回归**:`python -m unittest discover -s tests` 全绿(现有测试不受影响)。 - **真机 smoke**: - `python model_decimate.py src/well1500.fbx --tris 5000 --reducer quad -o out_qr` → 输出存在、`reducer=quad`、面数近似 5000(±20%)、UV 岛数/翻转/重叠 打印。 - 与 `--reducer collapse`(默认)对比 UV 指标,确认岛数下降/更规整。 - 强制回退验证:临时改 `QR_ENGINE` 指向不存在路径 → `reducer=quad->collapse`、 仍正常出 `_low.fbx`。 ## 明确不做(YAGNI) - 不改烘焙 `bl_bake.py`(QR 只作用于减面产出的低模)。 - 不做二次 collapse 卡精确面数。 - 不暴露 QR 全部参数(对称/顶点色/材质引导);只用 target/adaptive/hard_edges 合理默认。 - 不动进行中的 `2026-07-16-model-bake-quality-optimization` 计划;本功能与其正交, 可后续叠加(quad 拓扑 + 法线修复不冲突)。