Files
FiDA-3D-Trellis/0_remesh.py

261 lines
8.4 KiB
Python
Raw Permalink Normal View History

2026-03-17 11:28:52 +08:00
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