ModelTranslator: QR watertightness gate + quad default

根因(真机排查):QuadRemesher 引擎在激进减面下非确定性地从封闭流形
输入打出孔洞/非流形边(实测同一输入约 85% 次数破碎),即"破碎面"。

修复:QR 导回后比对输入拓扑,若比输入新增开边(孔洞)/非流形边即判破损、
弃用 QR 回退 collapse(topology_acceptable 纯策略+单测;_edge_defects 计数)。
顺带修复水密门在导入后拒绝时未恢复 obj 选中态,导致 collapse 回退时
export use_selection 选不到对象、只导出 4KB 空 FBX 的 bug。

含用户改动:--reducer 默认改为 quad(配合水密门,破碎自动回退,安全),
及其 CLI 默认值测试。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ud18010
2026-07-23 10:10:45 +08:00
co-authored by Claude Opus 4.8
parent 44060ac3cb
commit 0481f10b8c
6 changed files with 103 additions and 6 deletions
+26 -1
View File
@@ -117,10 +117,22 @@ def _decimate(obj, ratio):
_apply_modifier(obj, mod)
def _edge_defects(obj):
"""(开边数, 非流形边数):开边=只挂 1 面(孔洞边界),非流形=挂 >2 面。
用于 QR 输出水密门——QR 引擎在激进减面下会非确定性地打出孔洞/非流形。"""
import bmesh
bm = bmesh.new()
bm.from_mesh(obj.data)
boundary = sum(1 for e in bm.edges if len(e.link_faces) == 1)
nonman = sum(1 for e in bm.edges if len(e.link_faces) > 2)
bm.free()
return boundary, nonman
def _remesh_quad(obj, target_tris, warnings):
"""Quad Remesher 重拓扑 objheadless 直调 xremesh.exe,绕开 modal 操作符)。
成功返回 True,并把导回的 quad 网格设为新 active object(原 obj 已删);
任何失败记 warning 返回 False,交上层回退 collapse。"""
任何失败——含 QR 输出破损未过水密门——记 warning 返回 False,交上层回退 collapse。"""
import bpy
import subprocess
import tempfile
@@ -142,6 +154,7 @@ def _remesh_quad(obj, target_tris, warnings):
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
src_boundary, src_nonman = _edge_defects(obj) # QR 输入拓扑基线
bpy.ops.export_scene.fbx(filepath=in_fbx, use_selection=True)
target_quads = qr_bridge.tris_to_target_quads(target_tris)
with open(settings, "w") as fp:
@@ -197,6 +210,18 @@ def _remesh_quad(obj, target_tris, warnings):
warnings.append("QR 导回无 mesh,回退 collapse")
return False
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))
bpy.data.objects.remove(retopo, do_unlink=True) # 弃坏网格,原 obj 完好
# FBX 导入把选中挪给了 retopo;恢复 obj 选中+active,供 collapse 与最终导出
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
return False
bpy.ops.object.select_all(action='DESELECT')
retopo.select_set(True)
bpy.context.view_layer.objects.active = retopo