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:
co-authored by
Claude Opus 4.8
parent
e7dcf71d35
commit
be09397609
@@ -6,6 +6,10 @@ import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
import qr_bridge
|
||||
|
||||
QR_POLL_INTERVAL = 0.3 # progress.txt 轮询间隔(秒)
|
||||
|
||||
TOLERANCE = 0.03 # 面数相对误差容忍
|
||||
MAX_RETRY = 2 # ratio 修正轮数上限
|
||||
|
||||
@@ -112,6 +116,92 @@ def _decimate(obj, ratio):
|
||||
_apply_modifier(obj, mod)
|
||||
|
||||
|
||||
def _remesh_quad(obj, target_tris, warnings):
|
||||
"""Quad Remesher 重拓扑 obj(headless 直调 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):
|
||||
"""减面前清理:按局部包围盒对角线相对焊接重合点 + 去退化面,给 collapse 更干净的流形,
|
||||
减少被迫加的 seam。各步失败降级不中断。"""
|
||||
@@ -392,6 +482,7 @@ def main():
|
||||
src, out_fbx, target = argv[0], argv[1], int(argv[2])
|
||||
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
|
||||
reducer_arg = argv[5] if len(argv) > 5 else "collapse"
|
||||
warnings = []
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
@@ -408,8 +499,20 @@ def main():
|
||||
|
||||
_clean_mesh(obj, warnings)
|
||||
_triangulate(obj)
|
||||
orig = len(obj.data.polygons)
|
||||
orig = len(obj.data.polygons) # 高模三角面数(tris_before)
|
||||
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:
|
||||
warnings.append("当前 %d 面 <= 目标 %d,跳过减面" % (cur, target))
|
||||
else:
|
||||
@@ -445,6 +548,7 @@ def main():
|
||||
print("MT_SUMMARY " + json.dumps(
|
||||
{"src": os.path.basename(src), "fbx": os.path.basename(out_fbx),
|
||||
"tris_before": orig, "tris_after": cur, "target": target,
|
||||
"reducer": used_reducer,
|
||||
"uv": uv_info,
|
||||
"uv_preview": uv_preview,
|
||||
"uv_polys": uv_polys_json if uv_preview else None,
|
||||
|
||||
Reference in New Issue
Block a user