Files
FiDA-3D-Trellis/0_remesh.py
2026-04-13 11:20:56 +08:00

261 lines
8.4 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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