ModelTranslator: add QR quad remesh decimate backend with collapse fallback

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ud18010
2026-07-22 16:46:54 +08:00
co-authored by Claude Opus 4.8
parent e7dcf71d35
commit be09397609
+105 -1
View File
@@ -6,6 +6,10 @@ import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import qr_bridge
QR_POLL_INTERVAL = 0.3 # progress.txt 轮询间隔(秒)
TOLERANCE = 0.03 # 面数相对误差容忍 TOLERANCE = 0.03 # 面数相对误差容忍
MAX_RETRY = 2 # ratio 修正轮数上限 MAX_RETRY = 2 # ratio 修正轮数上限
@@ -112,6 +116,92 @@ def _decimate(obj, ratio):
_apply_modifier(obj, mod) _apply_modifier(obj, mod)
def _remesh_quad(obj, target_tris, warnings):
"""Quad Remesher 重拓扑 objheadless 直调 xremesh.exe,绕开 modal 操作符)。
成功返回 True,并把导回的 quad 网格设为新 active object(原 obj 已删);
任何失败记 warning 返回 False,交上层回退 collapse。"""
import bpy
import subprocess
import tempfile
import time
engine = qr_bridge.find_qr_engine()
if not engine:
warnings.append("未找到 QuadRemesher 引擎(xremesh.exe),回退 collapse")
return False
work = os.path.join(tempfile.gettempdir(), "mt_qr")
os.makedirs(work, exist_ok=True)
in_fbx = os.path.join(work, "inputMesh.fbx").replace("\\", "/")
out_fbx = os.path.join(work, "retopo.fbx").replace("\\", "/")
prog = os.path.join(work, "progress.txt").replace("\\", "/")
settings = os.path.join(work, "RetopoSettings.txt")
for f in (out_fbx, prog):
if os.path.isfile(f):
os.remove(f)
try:
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
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:
fp.write(qr_bridge.build_settings(in_fbx, out_fbx, prog, target_quads))
proc = subprocess.Popen([engine, "-s", settings])
except Exception as e:
warnings.append("QR 启动失败(%s),回退 collapse" % e)
return False
t0 = time.time()
state = "running"
while True:
try:
with open(prog) as fp:
state, msg = qr_bridge.parse_progress(fp.read())
except IOError:
state, msg = "running", ""
if state == "success":
break
if state == "error":
warnings.append("QR 重拓扑失败(%s),回退 collapse" % msg)
return False
if proc.poll() is not None:
# 进程已退出:给 progress/输出一点落地时间后再判一次
time.sleep(QR_POLL_INTERVAL)
try:
with open(prog) as fp:
state, _ = qr_bridge.parse_progress(fp.read())
except IOError:
state = "running"
break
if time.time() - t0 > qr_bridge.QR_TIMEOUT:
proc.kill()
warnings.append("QR 超时(%ds),回退 collapse" % int(qr_bridge.QR_TIMEOUT))
return False
time.sleep(QR_POLL_INTERVAL)
if state != "success" or not (
os.path.isfile(out_fbx) and os.path.getsize(out_fbx) > 0):
warnings.append("QR 无有效输出,回退 collapse")
return False
try:
before = set(bpy.data.objects)
bpy.ops.import_scene.fbx(filepath=out_fbx)
new = [o for o in bpy.data.objects
if o not in before and o.type == 'MESH']
if not new:
warnings.append("QR 导回无 mesh,回退 collapse")
return False
retopo = new[0]
bpy.data.objects.remove(obj, do_unlink=True)
bpy.ops.object.select_all(action='DESELECT')
retopo.select_set(True)
bpy.context.view_layer.objects.active = retopo
return True
except Exception as e:
warnings.append("QR 导回失败(%s),回退 collapse" % e)
return False
def _clean_mesh(obj, warnings): def _clean_mesh(obj, warnings):
"""减面前清理:按局部包围盒对角线相对焊接重合点 + 去退化面,给 collapse 更干净的流形, """减面前清理:按局部包围盒对角线相对焊接重合点 + 去退化面,给 collapse 更干净的流形,
减少被迫加的 seam。各步失败降级不中断。""" 减少被迫加的 seam。各步失败降级不中断。"""
@@ -392,6 +482,7 @@ def main():
src, out_fbx, target = argv[0], argv[1], int(argv[2]) src, out_fbx, target = argv[0], argv[1], int(argv[2])
unwrap_mode = argv[3] if len(argv) > 3 else "seam" unwrap_mode = argv[3] if len(argv) > 3 else "seam"
overlap_tol = float(argv[4]) if len(argv) > 4 and argv[4] else UV_OVERLAP_TOL overlap_tol = float(argv[4]) if len(argv) > 4 and argv[4] else UV_OVERLAP_TOL
reducer_arg = argv[5] if len(argv) > 5 else "collapse"
warnings = [] warnings = []
bpy.ops.wm.read_factory_settings(use_empty=True) bpy.ops.wm.read_factory_settings(use_empty=True)
@@ -408,8 +499,20 @@ def main():
_clean_mesh(obj, warnings) _clean_mesh(obj, warnings)
_triangulate(obj) _triangulate(obj)
orig = len(obj.data.polygons) orig = len(obj.data.polygons) # 高模三角面数(tris_before
cur = orig cur = orig
used_reducer = "collapse"
did_quad = False
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 not did_quad:
if cur <= target: if cur <= target:
warnings.append("当前 %d 面 <= 目标 %d,跳过减面" % (cur, target)) warnings.append("当前 %d 面 <= 目标 %d,跳过减面" % (cur, target))
else: else:
@@ -445,6 +548,7 @@ def main():
print("MT_SUMMARY " + json.dumps( print("MT_SUMMARY " + json.dumps(
{"src": os.path.basename(src), "fbx": os.path.basename(out_fbx), {"src": os.path.basename(src), "fbx": os.path.basename(out_fbx),
"tris_before": orig, "tris_after": cur, "target": target, "tris_before": orig, "tris_after": cur, "target": target,
"reducer": used_reducer,
"uv": uv_info, "uv": uv_info,
"uv_preview": uv_preview, "uv_preview": uv_preview,
"uv_polys": uv_polys_json if uv_preview else None, "uv_polys": uv_polys_json if uv_preview else None,