261 lines
8.4 KiB
Python
Executable File
261 lines
8.4 KiB
Python
Executable File
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
|