import bpy import sys import os import argparse import tempfile def clean_scene(): bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete(use_global=False) def _parse_args_blender(argv): """Blender 参数在 `--` 之后。""" if "--" in argv: return argv[argv.index("--") + 1:] return [] def _is_glb_gltf(path: str) -> bool: ext = os.path.splitext(path)[1].lower() return ext in [".glb", ".gltf"] def _is_obj(path: str) -> bool: return os.path.splitext(path)[1].lower() == ".obj" def import_mesh_any(input_path: str): """ 导入 OBJ 或 GLB/GLTF,并将场景中所有 MESH 合并为一个 active mesh 对象返回。 """ clean_scene() if _is_glb_gltf(input_path): print(f"[import] GLB/GLTF: {input_path}") bpy.ops.import_scene.gltf(filepath=input_path) elif _is_obj(input_path): print(f"[import] OBJ: {input_path}") bpy.ops.wm.obj_import(filepath=input_path) else: raise RuntimeError(f"Unsupported input format: {input_path}") meshes = [o for o in bpy.context.scene.objects if o.type == "MESH"] if not meshes: raise RuntimeError("No mesh objects found after import.") bpy.ops.object.select_all(action='DESELECT') for o in meshes: o.select_set(True) bpy.context.view_layer.objects.active = meshes[0] if len(meshes) > 1: bpy.ops.object.join() obj = bpy.context.view_layer.objects.active obj.select_set(True) return obj def export_obj_selected(output_obj: str): out_dir = os.path.dirname(output_obj) if out_dir: os.makedirs(out_dir, exist_ok=True) bpy.ops.wm.obj_export( filepath=output_obj, export_selected_objects=True, export_normals=True, export_uv=True, ) def fix_mesh(obj): """修复网格使其更符合 QuadriFlow 的输入要求""" bpy.context.view_layer.objects.active = obj obj.select_set(True) bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.remove_doubles(threshold=0.001) bpy.ops.mesh.delete_loose() bpy.ops.mesh.select_all(action='DESELECT') bpy.ops.mesh.select_non_manifold() selected_count = sum(1 for v in obj.data.vertices if v.select) if selected_count > 0: print(f"[fix_mesh] found {selected_count} non-manifold verts, deleting...") bpy.ops.mesh.delete(type='VERT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.normals_make_consistent(inside=False) bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.fill_holes(sides=0) bpy.ops.mesh.remove_doubles(threshold=0.001) bpy.ops.mesh.dissolve_degenerate(threshold=0.0001) bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.normals_make_consistent(inside=False) bpy.ops.object.mode_set(mode='OBJECT') def quadriflow_remesh_obj( input_obj: str, output_obj: str, face_count: int = 8000, use_mesh_symmetry: bool = False, use_preserve_sharp: bool = False, use_preserve_boundary: bool = True, use_voxel_preprocess: bool = True, voxel_size: float = 0.008, ): """只处理 OBJ 输入(你的原流程)""" obj = import_mesh_any(input_obj) # 这里会走 OBJ import print(f"[import] object={obj.name}, verts={len(obj.data.vertices)}, faces={len(obj.data.polygons)}") print("[mesh] fixing...") fix_mesh(obj) if use_voxel_preprocess: print(f"[voxel] preprocess voxel_size={voxel_size} ...") voxel_mod = obj.modifiers.new(name="Voxel", type='REMESH') voxel_mod.mode = 'VOXEL' voxel_mod.voxel_size = voxel_size voxel_mod.use_smooth_shade = True voxel_mod.use_remove_disconnected = False bpy.ops.object.modifier_apply(modifier=voxel_mod.name) print(f"[voxel] after: verts={len(obj.data.vertices)}, faces={len(obj.data.polygons)}") print("[mesh] fixing after voxel...") fix_mesh(obj) print(f"[mesh] after fix2: verts={len(obj.data.vertices)}, faces={len(obj.data.polygons)}") print(f"[quadriflow] target_faces={face_count} ...") bpy.context.view_layer.objects.active = obj obj.select_set(True) try: bpy.ops.object.quadriflow_remesh( use_mesh_symmetry=use_mesh_symmetry, use_preserve_sharp=use_preserve_sharp, use_preserve_boundary=use_preserve_boundary, smooth_normals=False, mode='FACES', target_faces=face_count, seed=0 ) print("[quadriflow] done.") except Exception as e: print(f"[quadriflow] error: {e} (continue to export)") print(f"[export] {output_obj}") export_obj_selected(output_obj) print(f"[done] verts={len(obj.data.vertices)}, faces={len(obj.data.polygons)}") def glb_to_tmp_obj(input_glb: str) -> str: """ 在 Blender 内把 GLB/GLTF 导入后导出为临时 OBJ(按你的要求“先转OBJ再走后续”)。 """ obj = import_mesh_any(input_glb) # 会走 gltf import tmp_obj = tempfile.mktemp(suffix=".obj") print(f"[glb2obj] export tmp obj -> {tmp_obj}") export_obj_selected(tmp_obj) return tmp_obj def run_pipeline( input_path: str, output_obj: str, face_count: int, mesh_symmetry: bool, preserve_sharp: bool, preserve_boundary: bool, no_voxel_preprocess: bool, voxel_size: float, ): """ 统一入口: - 输入 OBJ:直接 remesh - 输入 GLB/GLTF:先转临时 OBJ,再 remesh """ tmp_obj = None try: if _is_glb_gltf(input_path): tmp_obj = glb_to_tmp_obj(input_path) quadriflow_remesh_obj( input_obj=tmp_obj, output_obj=output_obj, face_count=face_count, use_mesh_symmetry=mesh_symmetry, use_preserve_sharp=preserve_sharp, use_preserve_boundary=preserve_boundary, use_voxel_preprocess=(not no_voxel_preprocess), voxel_size=voxel_size, ) elif _is_obj(input_path): quadriflow_remesh_obj( input_obj=input_path, output_obj=output_obj, face_count=face_count, use_mesh_symmetry=mesh_symmetry, use_preserve_sharp=preserve_sharp, use_preserve_boundary=preserve_boundary, use_voxel_preprocess=(not no_voxel_preprocess), voxel_size=voxel_size, ) else: raise RuntimeError(f"Unsupported input format: {input_path}") finally: if tmp_obj and os.path.exists(tmp_obj): try: os.remove(tmp_obj) print(f"[cleanup] removed tmp obj: {tmp_obj}") except Exception: pass def main(): parser = argparse.ArgumentParser(description="GLB/GLTF/OBJ -> (optional tmp OBJ) -> QuadriFlow remesh -> OBJ") parser.add_argument("-i", "--input", default="trellis_out/sample.glb", help="Input file path (.obj/.glb/.gltf).") parser.add_argument("-o", "--output", default="output/test.obj", help="Output OBJ file path.") parser.add_argument("--face_count", type=int, default=8000, help="Target quad face count.") parser.add_argument("--mesh_symmetry", action="store_true", help="Enable mesh symmetry.") parser.add_argument("--preserve_sharp", action="store_true", help="Preserve sharp edges.") parser.add_argument("--preserve_boundary", action="store_true", help="Preserve boundary edges.") parser.add_argument("--no_voxel_preprocess", action="store_true", help="Disable voxel preprocess.") parser.add_argument("--voxel_size", type=float, default=0.008, help="Voxel size for preprocess (smaller=denser).") args = parser.parse_args(_parse_args_blender(sys.argv)) if not os.path.exists(args.input): raise FileNotFoundError(f"Input not found: {args.input}") run_pipeline( input_path=args.input, output_obj=args.output, face_count=args.face_count, mesh_symmetry=args.mesh_symmetry, preserve_sharp=args.preserve_sharp, preserve_boundary=args.preserve_boundary, no_voxel_preprocess=args.no_voxel_preprocess, voxel_size=args.voxel_size, ) if __name__ == "__main__": try: main() except Exception as e: print(f"[fatal] {e}") import traceback traceback.print_exc() raise