From 5fda5d6bef8909ae296b090e517ee04a3a7288c6 Mon Sep 17 00:00:00 2001 From: ud18010 Date: Thu, 23 Jul 2026 15:00:46 +0800 Subject: [PATCH] ModelTranslator: quad backend precollapse + fill-holes + keep-quads Co-Authored-By: Claude Opus 4.8 --- Tools/ModelTranslator/bl_decimate.py | 35 ++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/Tools/ModelTranslator/bl_decimate.py b/Tools/ModelTranslator/bl_decimate.py index 7865b428..273c089c 100644 --- a/Tools/ModelTranslator/bl_decimate.py +++ b/Tools/ModelTranslator/bl_decimate.py @@ -1,6 +1,6 @@ """Blender 内运行:FBX 减面到指定三角面数 + UV 重展(锐边 seam 或 Smart UV Project)。 调用:blender -b --factory-startup --python bl_decimate.py -- - [unwrap:seam|smart] [overlap_tol] [reducer:collapse|quad] + [unwrap:seam|smart] [overlap_tol] [reducer:collapse|quad] [qr_input_cap] """ import os import sys @@ -129,6 +129,21 @@ def _edge_defects(obj): return boundary, nonman +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) + + def _remesh_quad(obj, target_tris, warnings): """Quad Remesher 重拓扑 obj(headless 直调 xremesh.exe,绕开 modal 操作符)。 成功返回 True,并把导回的 quad 网格设为新 active object(原 obj 已删); @@ -210,11 +225,14 @@ def _remesh_quad(obj, target_tris, warnings): warnings.append("QR 导回无 mesh,回退 collapse") return False retopo = new[0] - # 水密门:QR 引擎非确定性地会打出孔洞/非流形,破损即弃用 QR 回退 collapse + # 补小洞: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): - warnings.append("QR 输出拓扑破损(开边 %d→%d,非流形 %d→%d),回退 collapse" + 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, do_unlink=True) # 弃坏网格,原 obj 完好 # FBX 导入把选中挪给了 retopo;恢复 obj 选中+active,供 collapse 与最终导出 @@ -515,6 +533,7 @@ def main(): # 直接调 bl_decimate 缺省用 collapse(安全确定性);model_decimate CLI 默认 quad, # 但总显式传第 6 位,故此兜底仅用于直接 Blender 调用,两者默认不同是有意为之 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 = [] bpy.ops.wm.read_factory_settings(use_empty=True) @@ -536,9 +555,15 @@ def main(): used_reducer = "collapse" did_quad = False 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) + cur = len(obj.data.polygons) # 预 collapse 后更新,QR 失败时 collapse 从此续减 if _remesh_quad(obj, target, warnings): obj = bpy.context.view_layer.objects.active # QR 导回的 quad 对象 - _triangulate(obj) # 三角化供导出/计数 + # 保四边:不三角化,直接导出四边网格(FBX/烘焙/Unity 均支持四边) cur = len(obj.data.polygons) used_reducer = "quad" did_quad = True